Skip to content

feat(cli): report workspace health in boss status and add boss doctor - #579

Merged
kshivang merged 3 commits into
risa-labs-inc:devfrom
Vidhaankhare16:feat/workspace-health-doctor
Sep 15, 2026
Merged

kshivang merged 3 commits into
risa-labs-inc:devfrom
Vidhaankhare16:feat/workspace-health-doctor

Conversation

@Vidhaankhare16

@Vidhaankhare16 Vidhaankhare16 commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Refs #418

boss status says it "Queries status and health of the running BOSS Console instance", but it only reports version, platform, project and memory. When the watchdog stops a plugin, the browser engine is missing or stuck, or an MCP fault is withholding tools, the only signal is a toast, a dialog or the bottom status bar. An agent driving BOSS from a terminal sees "running": true and nothing else. #418 asks for "one readable error" in that situation.

Change

  • boss status --json gains an additive health object: degraded, findings (area, severity, code, summary, optional subject and remedy), unchecked and partial. Human boss status gets one Health: line. Exit codes are unchanged.
  • New boss doctor [--json] prints the same report, one line per finding with a suggested next step. Exit 0 when healthy, 2 when degraded, 1 when BOSS is not running.
  • doctor is added everywhere a subcommand has to be listed: createBossCLI, the headless gate in CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS, and the three launchers.

Findings come from state BOSS already keeps:

Area Source Severity
plugins sandbox watchdog disables, and NEEDS_ATTENTION rows from the #454 health model, across open windows warning
browser no usable engine installed (FluckEngine.resolveEngineDir) warning
browser FluckEngine.initError, or the wedge detector's exhausted verdict critical
mcp killSwitchFault and policyFault critical when every tool is withheld, otherwise warning

It is read-only: nothing is enabled, reloaded or rewritten, and no recovery action is exposed over IPC. It does not fix the failures listed in #418, so #394, #399 and #410 stay open.

Notes for review

  • The snapshot code in currentHealthAction moved unchanged into currentPluginHealth(manager), which the feat(plugins): add health and recovery center #454 dialog and this report now share.
  • Each window registers a health source where its DefaultPlugin is created and removes it before the plugin is disposed. DefaultPlugin's init has already created the manager by then, so startup order is unchanged. With no window open, plugins are reported as unchecked.
  • The engine check does not use isEngineHealthy(), which is also false for an engine that is only closed and will be recreated.
  • Each area is read separately. An exception or linkage error marks that area unchecked instead of failing boss status. Plugin health goes further: it has one source per open window and each window source is contained on its own, so one window that throws cannot discard another window's findings. The area is unchecked only when no window could be read, and partial when some were read and some threw.
  • Faults other than an unreadable file map to a warning without listing their types, so the provider-level policy fault in feat(mcp): trust a whole plugin's tools at once, not one at a time #542 is still reported if that lands first.
  • buildStatusResponse takes its health JSON as a parameter, with the collector as the default, so a test can check the real response rather than a replaced one.
  • OsOpenArgumentsTest compared CLI_SUBCOMMANDS with the text of BossCommand.kt, which cannot see a subcommand declared in its own file. It now uses createBossCLI().registeredSubcommands().

Builds on #454 (@Ayush04-C) and #349 (@Kanakk23), and complements #436 (@Lavish-Bhatti) and #544 (@aryanbajaj868's work from #526). Issue by @surfthewave-commits.

Limits

Validation

Windows 11, Temurin 17 (as in CI), dev @ 109fb083.

  • Full :composeApp:desktopTest on the final code: 4577 tests in 476 classes, 15 skipped, 2 failures. Both failures need a Windows privilege this machine does not grant: DesktopFileScannerTest and DiscardDownloadContainmentTest create symlinks and fail with A required privilege is not held by the client. Every other test passes, including the new StatusHealthWiringTest.
  • ktlintCheck and detekt report nothing.
  • Manual run of the built jar with an isolated user.home:
    • BOSS not running: status and doctor exit 1 with the error on stderr only.
    • Clean profile with no browser engine downloaded: status exits 0 with Health: 1 problem; doctor exits 2 with a browser_engine_not_installed warning whose suggestion is the engine's own advice, and lists plugins as not checked, because a signed-out BOSS has no window yet.
    • Truncated mcp-disabled-tools.json and mcp-tool-policy.json: doctor exits 2 with mcp_policy_unreadable and mcp_tools_withheld, one line each, including the kill-switch message that contains a line break; status still exits 0.
    • boss.bat doctor and boss.ps1 doctor --json forward the command and return 2.
  • Not run: macOS, Linux, or a signed-in build with a real watchdog stop.

No merge or auto-merge is authorized.

Review follow-up (2026-09-13)

Original health model, CLI, source wiring and initial tests are by @Vidhaankhare16; foundation credit to @Kanakk23 (#349) and @Ayush04-C (#454) remains above. Agent commit bb0cf4343 separates read-only cache inspection from startup repair recording and qualifies unchecked human reports, with regression tests. These repairs are agent work, not original contributor credit.

Not ready: one throwing window source still discards other windows' plugin health snapshots; preserve successful findings with explicit partial coverage. Kotlin tests, ktlint and detekt were queued under the shared local build lock but did not start, and hosted CI requires maintainer approval. Shell harness and diff checks pass. The new regressions and extracted-function detekt baseline need exact-head validation. See the author-facing review handoff for evidence and required cases.

@kshivang

Copy link
Copy Markdown
Contributor

@claude Please review workspace health/status and doctor at 9a826c8 against dev. Focus on actual health sources, lifecycle registration/removal, IPC behavior, diagnostic safety and meaningful tests. This is the first review request for this head.

@claude

claude Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Claude finished @kshivang's task in 6m 36s —— View job


Review: workspace health, boss status health, boss doctor @ 9a826c8

  • Gather context (diff, new files, surrounding subsystems)
  • Health sources: are they the right signals, read safely?
  • Lifecycle: per-window source registration / removal
  • IPC behavior (status response shape, timeouts, size)
  • Diagnostic safety (side effects, message pass-through, path/content disclosure)
  • Tests: do they pin behavior that matters?

The shape of this is right: a pure report function, injectable sources, containment per area, an additive wire contract, and a CLI that reads nothing but the health object. Below are one correctness finding I'd want fixed before merge, two design questions about what "degraded" means, and three smaller items.


1. The browser probe is not read-only on macOS, and can burn a one-shot repair budget

currentBrowserEngineHealth() calls FluckEngine.resolveEngineDir() with its default argument (composeApp/src/desktopMain/kotlin/ai/rever/boss/health/WorkspaceHealthCollector.kt:76). That default is cacheIsHealthy() → ChromiumAutoDownloader.isChromiumInstalled(), which is not a predicate: on macOS, when the cached engine still declares CFBundleURLTypes/CFBundleDocumentTypes, it calls recordRepairAttempt() and writes ~/.boss/boss-chromium.types-repair (ChromiumAutoDownloader.kt:229-251).

That marker is the one-attempt-per-version bound AGENTS.md documents for the engine-bundle repair. The health collector spends it without downloading anything, and the window is real rather than theoretical:

  • SingleInstanceManager.acquireLock() → startServer() publishes the channel at main.kt:457, so STATUS is answerable from that point on.
  • The startup engine check is ~290 lines later at main.kt:745.

A boss doctor (or any boss status) served in between makes the collector the first caller in the process. It records the attempt, reports the engine as browser_engine_not_installed, and downloads nothing — and then main.kt:745 asks again, sees repairAlreadyAttempted(), logs "keeping it", and skips the re-download. The hijacking engine stays, on every launch after that. It also re-logs a WARN per status query in the already-repaired state.

This also makes the docs claim in docs/CLI.md ("Both commands are read-only: they never change plugin, browser or MCP state") depend on a main.kt ordering rather than on the code. Suggested fix: split isChromiumInstalled() into a pure predicate plus the repair-recording wrapper and pass the pure value as resolveEngineDir(cacheHealthy = …), with a test asserting a health collection never touches the marker. Fix this →

2. Any warning makes the workspace "degraded", so the exit code is permanently 2 on many installs

WorkspaceHealthReport.degraded is findings.isNotEmpty() and BossDoctorCommand.kt:38 exits 2 on that. The PR lists this under Limits, but I'd push back on it rather than accept it: browser_engine_not_installed is the normal steady state for any install that never downloaded the engine, so boss doctor exits 2 forever and boss status prints Health: 1 problem forever. An exit code that is always 2 is one agents learn to ignore, which defeats the purpose of adding it.

The severity enum already carries the distinction. Exiting 2 only when something is critical (with warnings still listed, and a --strict flag if the current behaviour is wanted) would keep the code meaningful without hiding anything. Fix this →

3. An all-unchecked report prints "No problems found." and exits 0

formatDoctorReport (BossDoctorCommand.kt:64) prints No problems found. whenever findings is empty, then appends Not checked: …. If all three sources fail — exactly the case the containment in WorkspaceHealthCollector.read exists for — the tool reports a clean bill of health and exits 0, and "degraded": false says the same to a script. The KDoc's "neither healthy nor degraded" is the right intent, but neither the wire field nor the exit code can express it.

At minimum the wording should not claim a clean result when areas were skipped. Worth deciding, too, whether all-unchecked deserves its own exit code, since "plugins unchecked because no window is open" is the common benign case and "every source threw" is not.

Related, in the same function: the human report branches on findings, but the exit code branches on degraded (BossDoctorCommand.kt:85). A future BOSS that sets degraded: true off something other than findings makes boss doctor print "No problems found." and exit 2. Fix this →

4. One window's failure blanks the plugins area for every window

WorkspaceHealthSources.pluginSnapshots() is pluginSources.values.map { it() } (WorkspaceHealthSources.kt:35), and the collector contains the whole call. So with three windows open, one source throwing (a manager mid-dispose, say) turns plugins into unchecked and silently discards the findings the other two windows would have contributed — the "watchdog stopped a plugin" case that motivated the feature. The containment decision is documented, but per-source would be strictly better: keep what could be read, and only mark unchecked when nothing could. Fix this →

5. Fault text reaches the IPC response and stdout raw and unbounded

killSwitchFinding uses fault.message as the summary (WorkspaceHealth.kt:202). For PersistedSetUnreadable, error is t.message from json.decodeFromString (McpToolRegistryImpl.kt:1001), and kotlinx appends the offending document to a malformed-input error — the same property this repo's AGENTS.md warns about for Supabase payloads. So the contents of mcp-disabled-tools.json can be inlined into the status response and then re-flowed onto one line by text(). The file holds tool names rather than secrets, so this is a bounding issue rather than a leak, but the messages also carry absolute paths (hence usernames), and EngineInitError.Other passes e.message through verbatim.

Suggest bounding summary length — buildLlmTokenResponse in the same file already does .take(180) on an error message for this reason. The 4 MB MAX_DATA_RESPONSE_BYTES cap is the only limit today. Fix this →


What I checked and found sound

  • Lifecycle. Registration sits after DefaultPlugin(...) (so the manager exists) and unregistration is the first statement of onDispose, before plugin.dispose() — the ordering the comment claims. unregisterPlugins using ConcurrentHashMap.remove(key, value) with the captured lambda instance is the right shape for a keyed DisposableEffect that can re-run for the same windowId, and WorkspaceHealthSourcesTest pins exactly that. No leak beyond one entry per live window.
  • Thread safety. The IPC handler thread reads only StateFlow.value and PluginSandboxManager.disabledPlugins, which is a ConcurrentHashMap.newKeySet (PluginSandboxManager.kt:184). No lock is taken on the read path, so a status query cannot deadlock behind the UI thread or a plugin load — worth keeping that way if currentPluginHealth grows.
  • IPC additivity. health is added inside the existing buildJsonObject, exit codes unchanged, and healthSummaryOf returns null for a response without the field, so a new CLI against an old BOSS prints no Health: line rather than a wrong one. BossDoctorCliTest covers both directions.
  • Headless path. BossDoctorCommand imports nothing but SingleInstanceManager and kotlinx-json, so the headless gate in main.kt does not drag FluckEngine into a GUI-less JVM. Good, and easy to regress.
  • The currentPluginHealth extraction is behaviour-preserving for currentHealthAction, and pulling it out of composition is what makes it callable off the UI thread.
  • OsOpenArgumentsTest asking createBossCLI().registeredSubcommands() instead of regexing BossCommand.kt is a real improvement — the old test structurally could not see a subcommand in its own file.
  • Launchers propagate the code (exec in scripts/boss, exit /b %ERRORLEVEL% in boss.bat), and doctor is in the dispatch branch, not just the help text, in all three.
  • Docs are unusually complete for a wire contract (codes listed, unchecked explained, exit 2 scoped to doctor), and no em-dashes were added.

Smaller notes

  • pluginFindings dedupes attention rows with distinctBy { it.pluginId to it.detail } (WorkspaceHealth.kt:143), so one plugin reported with different details in two windows yields two findings for one plugin. The PR's "reported once" limit holds only for identical details.
  • StatusHealthWiringTest proves the field is wired but always injects healthJson, so nothing exercises the default — the real WorkspaceHealthCollector(). A smoke test that collect() with default sources neither throws nor writes anything would cover both the wiring and finding 1.
  • The headless set in main.kt:252 is deliberately a subset of the registered subcommands, so the new OsOpenArgumentsTest equality cannot pin it; a subset assertion would still catch a typo there.
  • PluginHealthSnapshot.sandboxDisabledPluginIds is derived by filtering states.keys, so a watchdog-disabled plugin absent from pluginStates is not reported. Same as the existing dialog, so not a regression — just not a guarantee the report can make.

I did not run Gradle in this environment, so ktlintCheck, detekt and desktopTest are unverified here; I reviewed against the PR's own Windows run.
· branch feat/workspace-health-doctor

@kshivang

Copy link
Copy Markdown
Contributor

@Vidhaankhare16 Reviewed against dev e1926cc6f; current Claude review: #579 (comment).

I pushed bounded agent fixes in bb0cf4343 (separate from your contribution):

  • Health cache inspection no longer invokes the repair-recording path. Startup retains the same recording behavior through the shared chromiumInstalledAt predicate. The regression builds a disposable stale macOS bundle, checks repeated read-only probes leave its repair marker absent, then checks startup still records exactly once. No real profile or engine was touched.
  • Human status names unchecked areas; doctor qualifies incomplete reports and does not say no problems when degraded=true has no details. Added actual CLI/socket cases. Existing warning/exit-2 policy remains as your documented design; changing it to critical-only is not a correctness requirement.
  • The existing ReturnCount baseline follows the extracted cache predicate; no new class-wide suppression.

Outstanding author issue: WorkspaceHealthSources.pluginSnapshots() maps all windows without per-source containment. One throwing source discards successful snapshots from other windows, so a watchdog-stopped plugin in window A disappears when window B's source throws. Please preserve successful snapshots while explicitly reporting partial coverage, and test a failing source beside a stopped-plugin source. Do not silently label partial results complete. This is independent of the new read-only repair.

Validation blockers are separate: shell harness and git diff --check pass. Gradle tests plus ktlint/detekt were requested through the shared build-lock wrapper, but waited without starting; no Kotlin success is claimed. The expanded validation request includes WorkspaceHealth*, BossDoctorCliTest, StatusHealthWiringTest, OsOpenArgumentsTest and ChromiumAutoDownloaderTest. Hosted Build & Test run 34601195267 is action_required, a maintainer approval gate, not a failing assertion. Please validate the pushed tests and baseline identity when a runner is available, including the exact final head. Current code paths were inspected, but the candidate repairs are not represented as test-validated.

Optional diagnostic-length limits and unknown-health exit semantics remain review notes, not requests to redesign this feature. After this bounded repair attempt, the partial-window correctness case and required validation remain unresolved, so this is NOT ready; converting to draft under the handoff policy. Your implementation and authorship, including credit for #349/#454 foundations, are preserved. No merge or auto-merge.

@kshivang
kshivang marked this pull request as draft September 13, 2026 04:00
@Vidhaankhare16
Vidhaankhare16 force-pushed the feat/workspace-health-doctor branch from bb0cf43 to 6cf8ebc Compare September 13, 2026 07:46
@Vidhaankhare16

Copy link
Copy Markdown
Contributor Author

Thank you for bb0cf4343, and for fixing both of those yourself rather than bouncing them back. I have rebased onto it rather than over it: your commit is in the history as f9c710e3 after the rebase, authored by you, and I have left both of its fixes untouched. isChromiumInstalledReadOnly() is still what the health probe calls, isChromiumInstalled() still owns the one-shot repair marker, and your test read-only cache inspection leaves the startup repair attempt available passes on the rebased head. I have also taken your ruling on the exit-code question and changed nothing there.

On the one outstanding defect you named: pluginSnapshots() now contains each window source individually instead of mapping them in one expression. Every snapshot that could be read is kept, each failure is counted, and each failure is logged through BossLogger with LogCategory.SYSTEM the same way the collector's unreadable() already logs an unreadable area. LinkageError is caught beside Exception for the same reason the collector catches it. A watchdog-stopped plugin in window A now survives window B's source throwing.

Preserving those snapshots meant the report needed a way to say the coverage was incomplete, because unchecked is binary per area and cannot express it. Rather than overload unchecked, I added an additive partial set beside it. unchecked keeps its exact meaning, nothing could be read at all; partial means an area was read from several sources and some of them failed, so the findings listed are real but do not cover the area. An area is never in both: if every window source fails, nothing was read, so the area is unchecked and not partial. Today only plugins can be partial, since it is the only area with one source per window.

Partial coverage is stated in words, not just on the wire. boss doctor prints a Partially checked: plugins. Some sources could not be read, so problems there may be missing. line, and it no longer prints No problems found. when an area is partial; it prints No problems found in checked areas. boss status appends partially checked: plugins to its Health: line. Neither affects the exit code, matching how unchecked already behaves.

The partial field is additive in both directions and docs/CLI.md documents it with the rest of the wire contract. A reader that does not know the field sees exactly what it saw before, and both CLI commands read an absent partial as empty, so an old CLI against a new BOSS and a new CLI against an old BOSS both still parse. There is a test for that second direction.

The test you asked for is a failing window source does not hide a stopped plugin in another window in WorkspaceHealthSourcesTest. It registers a real failing source beside a real source reporting a sandbox-stopped plugin, runs them through the actual WorkspaceHealthCollector, and asserts all three things: the stopped plugin is still reported, plugins is not unchecked, and the report declares plugins partial. Alongside it: a sources test that a failing source is counted while the others are still read (covering both Exception and LinkageError), collector tests that snapshots survive a failing source and that all-sources-failed is unchecked rather than partial, report and JSON tests for the partial semantics, and four BossDoctorCliTest cases covering the doctor line, the status line, both qualifiers together, and the absent-field back-compatibility case.

The branch is also rebased onto dev at 109fb083. That was forced by #450 landing, which moved the headless CLI dispatch out of main.kt; main.kt was the only conflicting file. doctor now joins the headless command set in CliBootstrap.isHeadlessCli instead, CliBootstrapTest pins it there, and main.kt is now byte-identical to dev. OsOpenArguments.CLI_SUBCOMMANDS is unaffected by the move and OsOpenArgumentsTest still checks it against createBossCLI().registeredSubcommands().

Validation on the rebased head, JDK 17: :composeApp:desktopTest ktlintCheck detekt. 4577 tests across 476 classes, 15 skipped, 2 failures. Both failures are environmental on my Windows machine and fail identically on untouched dev: DesktopFileScannerTest > directoryHasChildren follows a symlink to a non-empty directory and DiscardDownloadContainmentTest > a symlink inside staging pointing out is refused, both needing symlink creation privilege. ktlint and detekt are clean. As on the other PR, I have left this as a draft rather than flipping it myself, since you converted it under your own validation policy: tell me if you would like it marked ready for review, or flip it yourself. No merge or auto-merge is authorized.

@Vidhaankhare16

Copy link
Copy Markdown
Contributor Author

Rebased onto the current dev at f91633d3. This PR reported mergeable: CONFLICTING because dev was rewritten upstream rather than fast-forwarded: 109fb083, the commit this branch was previously rebased onto, is no longer an ancestor of dev, and the same merged PRs now appear under new SHAs (for example #450 as 2197b80d and #505 as f91633d3). The rebase used git rebase --onto origin/dev 109fb083, which replays only this branch's own three commits.

Your commit is preserved. fix(cli): keep health inspection read-only and qualify unchecked reports is now cda502a9, still authored by you; I confirmed it was present in the branch history before pushing. Otherwise no source change: the diff is the same 28 files and the same +1558/-53 as before the rebase, with identical file-set membership. New head is 698b840d. I am stating that explicitly because you asked that no further source change be made just to trigger CI: this push was only to restore mergeability after the upstream rewrite.

Validation on 698b840d, Windows, JDK 17: 4577 tests, 476 classes, 15 skipped, 2 failures. Both are the environmental pair this PR has disclosed throughout, DesktopFileScannerTest and DiscardDownloadContainmentTest, each failing with java.nio.file.FileSystemException when creating a symlink, which needs a privilege this machine does not grant. :composeApp:ktlintCheck and :composeApp:detekt both ran and passed; the only failing task was :composeApp:desktopTest. Not run on macOS or Linux.

No merge or auto-merge is authorized.

@kshivang
kshivang force-pushed the dev branch 4 times, most recently from cd88490 to 5a2e818 Compare September 14, 2026 11:16
Vidhaankhare16 and others added 3 commits September 14, 2026 17:15
boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from risa-labs-inc#454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs risa-labs-inc#418
…al coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs risa-labs-inc#418
@Vidhaankhare16
Vidhaankhare16 force-pushed the feat/workspace-health-doctor branch from 698b840 to 43d5688 Compare September 14, 2026 12:08
@Vidhaankhare16
Vidhaankhare16 marked this pull request as ready for review September 14, 2026 12:13
@kshivang

Copy link
Copy Markdown
Contributor

@claude Please review current head 43d5688 against current dev, focusing on the latest contributor revisions and prior handoff. One focused review requested for this sweep.

@kshivang

Copy link
Copy Markdown
Contributor

@Vidhaankhare16 Your follow-up at 43d56885 fixes the previous substantive handoff: each window source is contained independently, findings from responding windows survive, and partial accurately distinguishes incomplete coverage from wholly unchecked coverage. The additive JSON, human doctor/status qualifiers, registration ownership and headless CliBootstrap wiring are coherent. The original read-only cache repair is preserved; no new source edits were made by this sweep.

A focused compiled probe of the exact current WorkspaceHealthSources.kt passed: successful snapshots survive both Exception and LinkageError from other windows, failure counts are retained, and an old unregister cannot remove a replacement source. The probe uses a minimal snapshot/logging double, so it is not a full host/collector integration test. I inspected the contributor collector/report/CLI tests and actual cache-read implementation; I did not run a new full Gradle suite. The current head merges cleanly in a non-mutating simulation against dev.

The remaining gate is required validation, not an established author defect. I approved current-head Build & Test run https://github.com/risa-labs-inc/BossConsole/actions/runs/34841797578. Shell/version checks have passed, but build/test, code quality, compatibility and contracts remain queued at this disposition. Prior Windows evidence is retained, including the disclosed symlink-privilege failures; I am not reclassifying those as regressions here.

The latest completed Claude review predates this repair. Its per-window loss and read-only-cache concerns are addressed; diagnostic length limits and unknown-health exit semantics remain optional follow-ups, not reasons to redesign this feature. One fresh review was requested for this head and remains pending.

Converting to draft solely for the incomplete current-head validation under the sweep policy. Next steps: let the approved run finish, resolve any real failures with logs and baseline comparison, review the requested Claude result, then recheck current dev and mark ready. No source push is requested merely to trigger CI. Manual follow-up: boss status/boss doctor with a stopped plugin in one window and an unreadable second source, partial/all-unchecked output, and confirmation that status inspection leaves browser repair available. No recovery mutation or live app test was performed. Your original contribution and the separately authored earlier agent repairs remain credited.

@kshivang
kshivang marked this pull request as draft September 15, 2026 01:42
@claude

claude Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Claude finished @kshivang's task in 6m 51s —— View job


Review: workspace health, boss status health, boss doctor @ 43d5688

  • Gather context (diff vs base, new commits since last review)
  • Per-window containment + the new partial field
  • Read-only cache inspection fix (abf923a)
  • CLI / wire contract and back-compat
  • Tests: do they pin what matters?

The two blocking items from the last sweep are closed, and closed properly. Per-window containment keeps what it can read and says so on the wire rather than overloading unchecked; the read-only engine probe is sound on both of its branches, which is more than the fix strictly promised. One new correctness item below (small, concrete), plus notes.

Base note: origin/dev in this checkout is a depth-1 shallow ref (5dc8211, #593) and this branch's base is 5a2e818 (#661), so there is no merge base to diff from. I reviewed git diff 5a2e818...HEAD - the branch's own three commits - which is the PR diff (28 files, +1558/-53, matching the reported figures).


1. boss status and boss doctor disagree on degraded: true with no findings

abf923a hardened the doctor for a report that declares itself degraded without details (BossDoctorCommand.kt:69, tested at BossDoctorCliTest.kt:114-118). healthSummaryOf - the Health: line the same file serves to boss status - never reads that field:

val count = health.findings().size          // BossDoctorCommand.kt:98
…
count > 0 -> "${problemCount(count)} (run 'boss doctor')"
qualifiers.isEmpty() -> "OK"                // ← reached with degraded:true, findings:[]

So for {"degraded":true,"findings":[],"unchecked":[]}, boss status prints Health: OK while boss doctor on the same response prints "BOSS reports degraded health without finding details." and exits 2. Two commands, one instance, opposite stories - the shape of defect the Settings > Browser / Default Apps disagreement in AGENTS.md is recorded for.

Not reachable against this host (WorkspaceHealthReport.degraded is findings.isNotEmpty()), which is exactly why the doctor branch is forward-compat hardening - and the status line is the half that did not get it. One-line fix: let isDegraded() drive the count == 0 case in healthSummaryOf, and add the status mirror of the existing doctor test. Fix this →

2. The read-only probe is correct, and its two log lines are not

Verified rather than assumed, because the fix only removed the write:

  • currentBrowserEngineHealth passes isChromiumInstalledReadOnly() into resolveEngineDir(cacheHealthy = …), so cacheIsHealthy() (the recording variant) is never reached from the health path.
  • The other branch is clean too: noUsableEngineReason() → engineLocations() (BossDirectories.resolve + getBundledChromiumPath) → isValidChromiumDir / chromiumVersionMismatch, all reads. Had it gone through getChromiumDir() (FluckEngine.kt:1304, which calls resolveEngineDir() with the default) the marker would have been spent by the other door.
  • The only surviving recording callers are ChromiumBootstrap.kt:44 and FluckEngine.cacheIsHealthy(), i.e. startup and engine creation - which is the split the fix intended.
  • recordRepair has no default, so a new caller has to answer the question. Right call, same discipline as blockingDependentsOf's isDisabled.

What is left is log accuracy. chromiumInstalledAt still logs, on a read-only probe of a stale macOS bundle, either "Cached Chromium still registers itself as a browser, will re-download" (nothing will - the probe returns false and downloads nothing) or, once startup has spent the attempt, a WARN per status query. An agent polling boss doctor writes a WARN a tick into the host log, and the INFO line states an action that is not going to happen. Cheapest fix is to gate those two lines the same way the write is gated, or hand the function a log sink alongside recordRepair. Fix this →

3. unchecked still conflates "no window open" with "every window source threw"

plugins?.snapshots?.takeIf { it.isNotEmpty() } (WorkspaceHealthCollector.kt:34) sends both cases to unchecked, and docs/CLI.md documents the first as the normal reading ("including plugins while no BOSS window is open"). The first is benign and the common case; the second means every open window's manager threw, which is a fault. failedSources now distinguishes them and the report drops it on the floor (tested, deliberately, at WorkspaceHealthCollectorTest.kt:67).

Not a defect in what is documented - just the one place where the new data could answer a question the report still cannot. A plugin_health_unreadable finding, or listing the count, would make the difference visible without touching the unchecked/partial split.

4. The "empty means unchecked" rule lives in the collector, not in the model

workspaceHealthReport(plugins = emptyList()) reports plugins as fully checked and healthy - the null-means-unchecked encoding is only honoured because WorkspaceHealthCollector maps empty → null first. WorkspaceHealthReportTest.inputs defaults plugins to emptyList(), so the test helper's own default is the shape that would misreport if a second caller of the pure function ever appeared. Passing PluginSnapshotRead into workspaceHealthReport (it already takes pluginSourceFailures from it) would put both halves of that read in one place.

5. Carryover, unchanged by design

Prior item 5 (unbounded fault text) is still open and was ruled an optional note, so just the one-line restatement: killSwitchFinding uses fault.message verbatim (WorkspaceHealth.kt:218), and for PersistedSetUnreadable that embeds a kotlinx malformed-input message, which carries the offending document - so mcp-disabled-tools.json contents can ride the STATUS response and boss doctor stdout, bounded only by MAX_DATA_RESPONSE_BYTES. buildLlmTokenResponse in the same file already .take(180)s for this reason. Prior item 2 (warning ⇒ exit 2) stands as the documented design per the maintainer's ruling.

6. One observation about "read-only", worth a sentence in the docs

The health probe can be the first thing in the process to touch FluckEngine: the single-instance server is published by acquireLock() at main.kt:283, and the first FluckEngine reference in main.kt is at 533, inside composition. FluckEngine's init runs BrowserSettingsManager.ensureLoaded() (a read - loadSettingsSync only reads) and startHostThemeObserver(), which creates a process-wide CoroutineScope and a permanent snapshotFlow collector. Nothing is written and setColorScheme is a no-op with a null engine, so the outcome is benign - but a status query arriving in that window does start the engine singleton's observer earlier than the app would have, which is slightly more than docs/CLI.md's "never change plugin, browser or MCP state" claims. Same class of thing for McpToolRegistryImpl, whose init reads the disabled-tools file (and deliberately does not call onFault from the constructor, so no status message is stolen).


What I checked and found sound

  • Per-window containment (WorkspaceHealthSources.kt:59-74) is the right shape: LinkageError beside Exception, VirtualMachineError left alone, each failure logged with its windowId through BossLogger/LogCategory.SYSTEM, and the count returned rather than inferred. The registry iteration stays contained in the collector as belt-and-braces now that the inner loop cannot throw.
  • partial is a genuinely additive field, not an overload of unchecked, and the invariant "never both" is enforced by construction (partial is only added when plugins != null) and pinned three ways - pure function, collector, and end-to-end through the real socket. a failing window source does not hide a stopped plugin in another window asserts all three things at once (finding survives, not unchecked, declared partial) using real sources through the real collector, which is the test the handoff asked for.
  • Back-compat both directions. New CLI against an old BOSS: partialAreas() reads an absent key as empty and a BOSS that does not send partial is still read as fully checked pins Health: OK plus no "Partially checked". Old reader against a new BOSS: partial is a new key inside the existing object. HEALTHY in the test corpus deliberately omits the field.
  • Human output does not claim a clean result it cannot support. No problems found. / No problems found in checked areas. - and note the assertion at BossDoctorCliTest.kt:157 works because the qualified string does not contain the unqualified one as a substring, which is easy to get wrong the other way round.
  • ChromiumAutoDownloaderTest's new case is a real before/after regression, not a restatement: two read-only probes leave the marker absent, then a recording probe creates it exactly once, then inspect { error(...) } proves the one-shot holds. It builds a disposable stale bundle rather than touching a real engine.
  • Lifecycle is unchanged from the version I checked last time and still correct: registration after DefaultPlugin(...) (manager already constructed), unregistration as the first statement of onDispose before plugin.dispose(), and remove(key, value) against the captured lambda identity.
  • Thread safety of the off-UI read. currentPluginHealth reads StateFlow.values plus PluginCrashRegistry, whose _crashedPluginsMap is a ConcurrentHashMap explicitly documented "used by watchdog and non-UI queries", and PluginSandboxManager.disabledPlugins is a concurrent set. No lock on the read path, so a status query still cannot deadlock behind the UI thread.
  • detekt baseline follows the extracted predicate's exact signature (one ReturnCount entry moved, Path is imported unqualified as the entry renders it), with no new class-wide suppression - which is what the handoff asked for.
  • OsOpenArgumentsTest asking createBossCLI().registeredSubcommands(), CliBootstrap.isHeadlessCli carrying doctor with CliBootstrapTest pinning it, and all three launchers dispatching (not merely documenting) doctor - all still hold after the main.kt move. main.kt is untouched by this PR.
  • Docs. partial is documented with the invariant, the additivity and the exit-code neutrality; exit 2 is scoped to doctor. git diff --check is clean and no em-dash was added to any .md.

Validation status

I did not run Gradle in this environment, so ktlintCheck, detekt and :composeApp:desktopTest are unverified here. Note for the handoff: the PR's validation log names head 698b840d, and the head under review is 43d5688 - the tree was replayed again onto a newer dev after that run. If the rebase was diff-preserving the result should stand, but the exact-head run the handoff asked for has not been reported for this SHA.
· branch feat/workspace-health-doctor

@Vidhaankhare16
Vidhaankhare16 marked this pull request as ready for review September 15, 2026 03:55
@Vidhaankhare16

Copy link
Copy Markdown
Contributor Author

Run https://github.com/risa-labs-inc/BossConsole/actions/runs/34841797578 has completed. All ten checks are green on 43d56885aa7434858c40af9b1ec2836c684059c7: build-test on ubuntu, macOS and windows, code-quality, compatibility-test, BOSS AI contracts, Shell Script Tests, Database (pgTAP), version-check and build-summary. Required current-head validation was the gate you named, so I have marked this ready for review.

Rechecked against current dev, now 54ab77f6. The head still merges clean against it and is untouched, so the approval you granted covers exactly what is published.

Thank you for the compiled probe of WorkspaceHealthSources.kt. Confirming that successful snapshots survive a LinkageError as well as an Exception, and that an old unregister cannot remove a replacement source, goes past what my own tests establish, and the second of those is a case I had not covered.

The Claude review at this head

It closes both prior blockers and raises one new item. I would rather name it than let it pass quietly.

Item 1 is a real inconsistency, and I have reproduced it. healthSummaryOf derives the Health: line from the findings count alone and never reads degraded, while formatDoctorReport does. So for {"degraded":true,"findings":[],"unchecked":[]}, boss status prints Health: OK while boss doctor, reading the same response, prints "BOSS reports degraded health without finding details." and exits 2.

It is not reachable against this host. WorkspaceHealthReport.degraded is findings.isNotEmpty(), so that combination cannot be produced by the BOSS in this repo. It is forward-compat hardening, and abf923a gave it to the doctor half while the status half did not get it.

I have the fix written and validated, and I have deliberately not pushed it, because pushing discards the green run above and returns this PR to action_required. It is 3 files, +23/-1:

  • healthSummaryOf gains one branch, ordered after the findings-count branch so it can only fire on an explicit degraded: true with no findings.
  • One case in BossDoctorCliTest serves a single response to both commands and asserts they agree.
  • docs/CLI.md gains the degraded bullet. It was the only field in that object with no description, and the doc now states the reading rule instead of leaving it implied.

Evidence, applied on top of 43d56885 on Temurin 17:

  • Against the unpatched source the new test fails, and fails for the right reason: the assertion message is the status report showing Health: OK. 17 tests, 1 failure, no other case affected.
  • With the fix, BossDoctorCliTest is 17 tests, 0 failures, 0 skipped.
  • Full :composeApp:desktopTest: 4954 tests across 518 classes, 15 skipped, 2 failures, both the disclosed symlink-privilege ones on this Windows machine (DesktopFileScannerTest.directoryHasChildren follows a symlink to a non-empty directory and DiscardDownloadContainmentTest.a symlink inside staging pointing out is refused).
  • :composeApp:ktlintCheck clean, :composeApp:detekt clean with an empty checkstyle report.

Your call. Say the word and I will push it here and we spend one more CI approval on it, or it lands as a follow-up and this merges on the run that is already green. I am not pushing it unilaterally, given you said no source push is requested merely to trigger CI and this cannot fire at runtime today.

The remaining Claude items

My reading, offered rather than asserted. Item 3, unchecked conflating "no window open" with "every window source threw", would be a genuine improvement and failedSources already carries the data, but it changes what the wire means and belongs in its own change rather than riding this one. Item 4 is a fair note that the empty-means-unchecked rule lives in the collector instead of the model. Item 5 is the unbounded fault text you already ruled an optional note. Item 6 observes that a status query can be the first thing to touch FluckEngine, which is benign but slightly more than docs/CLI.md's "never change plugin, browser or MCP state" claims; worth one doc sentence whenever this file is next open.

Not claimed

The manual pass you named remains unperformed: boss status and boss doctor with a stopped plugin in one window and an unreadable second source, the partial and all-unchecked output shapes, and confirming that a status inspection leaves browser repair available. No app was launched from here, and no recovery mutation was exercised.

No merge or auto-merge is authorized.

@kshivang

Copy link
Copy Markdown
Contributor

@Vidhaankhare16 The required run is now green on exact head 43d56885aa7434858c40af9b1ec2836c684059c7. I verified the current Claude review, per-window source containment, ownership-safe unregister, read-only Chromium repair probe, partial/unchecked semantics and CLI routing. Current dev merges cleanly; its only change to a PR-touched file is unrelated detekt baseline entries. The headless launcher suite passes locally. No new source repair was needed in this batch.

The proposed degraded=true/empty-findings status consistency improvement is worthwhile follow-up, but the current host computes degraded from nonempty findings and cannot emit that shape. Please keep your prepared patch for a separate follow-up; it is not a reason to discard this validated head. The remaining log wording/repeated warning, singleton initialization, diagnostic bounds and all-unchecked distinction are recorded limitations, not new destructive health-read behavior. The read-only path never spends the repair marker. No repeated Claude request is needed.

Proceeding with a serialized final base/head/check refresh and squash merge into dev only. Original contribution credit and prior 7/10 rating are unchanged; earlier agent repairs are not scored as your new work. Manual checks still outstanding: partial/all-unchecked human output, stopped plugin in a second window, and confirming browser repair remains available after a health query. No live app or plugin was altered for this review.

@kshivang
kshivang merged commit 5f15d98 into risa-labs-inc:dev Sep 15, 2026
10 checks passed
kshivang added a commit that referenced this pull request Sep 15, 2026
…#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>
kshivang added a commit that referenced this pull request Sep 15, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Dow…
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachab…
kshivang added a commit that referenced this pull request Sep 17, 2026
)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Do…
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not r…
kshivang added a commit that referenced this pull request Sep 17, 2026
… a dismiss button (#154) (#742)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back …
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down…
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachab…
kshivang added a commit that referenced this pull request Sep 17, 2026
)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Do…
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not r…
kshivang added a commit that referenced this pull request Sep 17, 2026
… a dismiss button (#154) (#742)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back …
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down…
kshivang added a commit that referenced this pull request Sep 18, 2026
…ence (#876)

* test(browser): run the form field detector against a fake DOM (#609)

* test(browser): run the form field detector against a fake DOM

FormFieldDetector is JavaScript living inside a Kotlin string, and it is the
only one of those with no Kotlin test at all - so nothing read it, by grep or
by execution, while it was injected into every frame of every page from
BrowserHandleImpl.injectPageHelpers.

This executes both of its scripts against a fake DOM covering only what they
touch, so an added DOM read fails loudly. It follows test-browser-collector.js,
which exists for the same reason and whose own note records that every budget
bug in the collector so far was found by running it rather than reading it.

It also closes a coupling the Kotlin side cannot see. The injected script emits
nine keys and parseFieldInfoJson picks them back out with one hand-written
regex per key, across two files and two languages. A rename on either side does
not fail, it silently yields an empty string for that field forever. Both key
sets are read from source here and compared.

Four checks pin defects rather than desired behaviour. Each is marked and names
its follow-up, and they are pinned rather than fixed so the fix lands as a
reviewable diff against a measured baseline:

- The script has no re-injection guard. A second injection into one document
  leaves two listener pairs, and the host re-runs injectPageHelpers on every
  main-frame NavigationFinished, which for a single-page app is a route change
  within one document. BrowserInteractionScript guards exactly this case, and
  the harness reads that file to confirm the omission is not house style.
- Every focus is logged to the page's own console with the field's name or id.
- The focusout handler dereferences document.activeElement unguarded. That is
  nullable per spec, and the throw leaves the blurred field still referenced.
- parseFieldInfoJson matches "key":"([^"]*)" against JSON.stringify output, so
  a value containing a double quote is truncated and keeps a trailing
  backslash. The pattern is reconstructed from the Kotlin source rather than
  restated, so it cannot drift.

Sources are read line-ending normalised, since a Windows checkout with
core.autocrlf on has CRLF.

* test(browser): model textarea type and normal focus transitions

* test(browser): preserve independent timer failures and escaped values

* test(browser): assert normal blur timers complete without errors

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(updater): reject overlapping host plugin updates (#612)

* fix(updater): reject overlapping host plugin updates

* docs(updater): clarify admission scope and shared progress notice

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(run): preserve concurrent configuration updates (#755)

* fix(plugins): let a plugin's search provider reach global search (#670)

* fix(plugins): let a plugin's search provider reach global search

A dynamic plugin's register() receives a TrackingPluginContext around a
SandboxedPluginContext around the window's DefaultPlugin, and only
DefaultPlugin put a SearchProvider into SearchRegistryImpl. Neither
wrapper overrode registerSearchProvider or unregisterSearchProvider, so
the call reached the PluginContext default, whose body does nothing. The
bookmarks plugin registers its BookmarkSearchProvider this way, and
GlobalSearchService gets bookmark results only from that registry.

TrackingPluginContext now forwards both calls and records the teardown
with recordUiExtensionRegistration, like panel menus, settings pages,
deep-link handlers, shortcuts and status bar items, so unregisterAll
removes the provider when the plugin is disabled or unloaded.
SandboxedPluginContext forwards both calls like the registries above.

PluginContextWrapperForwardingTest checks that both wrappers declare
every PluginContext member. It uses Kotlin reflection because the api is
compiled with -jvm-default=enable, which adds a compatibility bridge for
each default method to implementing classes, so Java reflection reports
members a wrapper never overrode.

* fix(plugins): forward organisationMembersProvider and check inherited members

Rebased onto dev, which moved to plugin API 1.0.93 (#693). That api adds
PluginContext.organisationMembersProvider, which neither wrapper declared,
so PluginContextWrapperForwardingTest failed on the rebased branch for both
of them. Both wrappers now forward it. DefaultPlugin does not implement it
yet, so every plugin still gets null; the forwarding only keeps a future
host implementation from being swallowed by the wrappers.

From review:
- The guard now expects every member of PluginContext including inherited
  ones (less Any's), so splitting the api interface cannot quietly narrow
  it. A test with a small interface hierarchy pins that. Its KDoc states
  what it cannot see: members match by name, and a new wrapper has to be
  added to it.
- The TrackingPluginContext comments that list the recorded kinds, and the
  unregisterAll KDoc, now name search providers.
- AGENTS.md records search providers as the second three-wrapper gap.
- PluginRegistrationsAcrossWindowsTest (from #694) left search out of its
  teardown-chain test because the wrappers did not forward it; it now
  covers all seven kinds.

* test(fluck): cover zoom and favicon preservation across tab copies (#777)

* fix(fluck): preserve custom zoom level and favicon cache key across tab copy and split view

* style: fix ktlint trailing blank line format in FluckTabInfoTest

* fix(auth): read a pasted magic link the way the redirect function does (#740)

"Paste magic link manually" rebuilt a boss://auth/verify link only when the
pasted text contained a literal "token=". The sign-in email's link wraps the
confirmation URL in the redirect function's url= parameter, and a copied link
can carry it percent-encoded, so the paste was passed on as a web address,
dropped as unrouted, and the screen showed nothing.

Reproduced on BOSS 9.5.17 with a real email: the encoded link did nothing,
and the same token with only url= decoded signed in.

signInDeepLinkFor decodes the query and reads token or token_hash from the
link or its url= value, with the type beside the token, else the outer
link's, else magiclink, matching supabase/functions/redirect/app.ts. It
refuses a token or type with characters that could add parameters to the
rebuilt link, and a token on an address that is not a verify or redirect
link. Text that is not a sign-in link now shows an error instead of nothing.

* fix(tabs): keep tooltip visible while title updates (#752)

* fix(home): add persistent navigation in the active pane (#757)

* fix(home): offer direct Home entry from New Tab

* feat(home): add persistent pane-local Home navigation

* test(home): account for permanent Home in rail resize budget

* fix(home): remove redundant Home creation from New Tab

* fix(home): use subtle navigation selection instead of a filled strip

* fix(home): highlight navigation content without full-row hover fill

* fix(security): restrict plugin_ratings to the server-side rating pipeline (#761)

plugin_ratings' four RLS policies (20260130000000) carry no TO clause, so
they apply to anon and authenticated:

- SELECT USING (true) let any holder of the shipped anon key read every
  rating row across all users - auth.users UUID, free-text review, and
  timestamps, unscoped by plugin visibility, the same cross-user leak
  class closed for plugin_downloads by 20260910120000 (#487).
- The write policies only bind user_id to the caller, so a
  self-registered user can insert a rating for an arbitrary plugin UUID,
  using the FK violation as an existence oracle for org-scoped plugins.

The desktop client never touches this table directly: the plugin-store
Edge Function runs with the service role and routes/browse.ts reads
aggregates through the SECURITY DEFINER stats functions, so restricting
client access changes nothing for the shipped app.

Mirror the downloads migration: REVOKE ALL from PUBLIC/anon/
authenticated, GRANT the four verbs to service_role, drop the permissive
policies, and re-state the intent as explicit TO service_role policies.
pgTAP coverage asserts no non-service-role policy remains (unscoped
included), no per-verb client table privilege, and that service_role
keeps the live path.

* fix(ui): split the editor breadcrumb on path segments so it works on Windows (#753)

The bottom bar built the breadcrumb by appending "/" to the project path,
stripping that prefix and splitting on "/" alone. On Windows both the project
path and the tab's file path are native (C:\Users\dev\repo), so the prefix
never matched, the split found no separator, and the whole absolute path
rendered as a single segment.

Move the logic into editorBreadcrumbSegments, which treats both "/" and "\\"
as separators - the display-label rule plugin-path-utils documents and
extractFileName already follows - and compares whole segments, so a project at
/repo does not claim /repo2/App.kt. The helper lives in its own file to keep
BossBottomBar.kt under detekt's TooManyFunctions threshold.

Adds EditorBreadcrumbSegmentsTest: 9 cases, none host-dependent.

* fix(performance): read plugin process memory and threads per platform (#785)

The Performance panel read memory and thread counts for out-of-process
plugin JVMs with `ps -o pid=,rss=` and `ps -M` on every platform. `ps -M`
is the macOS thread listing. Windows has no `ps` (Git for Windows' MSYS
`ps` rejects both flags), so every plugin showed 0 bytes and 0 threads.
On Linux, procps reads `-M` as a security-label column and prints one
line per process, so every plugin showed exactly 1 thread.

Move the query into PluginProcessMetrics:
- Linux: VmRSS and Threads from /proc/<pid>/status, no subprocess.
- macOS: the same two ps commands and counting rule as before.
- Windows: Win32 through JNA, already a desktop dependency. Threads
  from a Toolhelp process snapshot, memory from GetProcessMemoryInfo.
  Not PowerShell: this runs every 5 s from the snapshot collector.

Rewrite the ps -M counting loop as a pipeline with the same rule, and
drop the two detekt baseline entries that named the deleted code.

Adds PluginProcessMetricsTest: 10 cases, 9 of them host-independent.

* fix(search): shift Spotlight file-name highlights by the platform separator (#716)

searchFiles matches the query against IndexedFile.relativePath, then
rebases the resulting ranges onto the file name, because FileResult's
matchRanges index the name. It found the name's start with
lastIndexOf('/'), but relativePath is derived from two absolute paths
and so is joined with a backslash on Windows. The search returned -1,
the ranges were shifted by nothing, and the dialog underlined whatever
characters happened to sit at those offsets: `uiButton` against
`app\ui\Button.kt` underlined "on" and "kt" rather than "Button", and a
match landing past the name's length was dropped and underlined nothing.

Look for the separator alongside '/', not instead of it. On a POSIX host
the two are the same character, so that host is unchanged; on Windows '/'
is not a legal file-name character and cannot split a name by accident. A
bare lastIndexOf('\') on every platform would be wrong, since a backslash
is a legal POSIX file-name character. ContentSearchService in this
package already reaches for File.separatorChar for this reason.

The separator is a parameter defaulting to the host's. Because the rule
is deliberately platform-dependent, a Windows-shaped path proves nothing
on a POSIX host - the shift there is correctly zero - so injecting it is
what lets the Windows case be asserted on every runner rather than only
on a Windows one. The four shift cases do that; the end-to-end case uses
the host separator and is red only on Windows, which its KDoc states.

* feat(plugins): add a copy button to plugin toasts (#745)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoti…
Vidhaankhare16 added a commit to Vidhaankhare16/BossConsole that referenced this pull request Sep 21, 2026
BOSS computes both halves of its own condition and exposed neither to a tool
caller. WorkspaceHealthReport is reachable only through SingleInstanceManager -
over the single-instance socket, from a CLI process outside the host - because
`boss doctor` was built for a human at a terminal (risa-labs-inc#418, risa-labs-inc#579). The performance
monitor publishes its snapshot to a settings panel and nowhere else. So the
agent inside the host, the party that has to decide whether to open another
browser tab or spawn another terminal, had no way to ask whether the host was
already degraded or near a ceiling; it found out by causing the failure.

Adds a host provider, boss-introspection, with get_workspace_health and
get_performance_metrics, both read-only by declaration rather than by the
mutating gate's fail-closed default.

The health tool returns WorkspaceHealthReport.toJson() - the same object
`boss status --json` emits - so the CLI and the MCP surface cannot drift into
two shapes for one concept, and scripts and agents match the same codes.

Counts and aggregates only. ResourceMetrics carries per-tab, per-terminal and
per-editor lists and CpuMetrics carries thread names; none of it travels. A
caller deciding whether it has headroom needs to know there are eleven browser
tabs, not which pages they are on.

A snapshot that does not exist yet reports sampling=false with no memory object
at all, never zeros, for the reason the health report separates unchecked from
"no findings": a caller reading 0 bytes used and concluding it has headroom is
worse off than one told the monitor has not reported.

Both sources are desktopMain and the provider is commonMain, so they arrive
through suppliers wired in main.kt the way WorkspaceMcpToolProvider.windowCreator
already is - which is also what lets the tests drive it with fakes.

The health payload is nested under "health" rather than flattened beside an
"available" flag, so the value is byte-identical to what `boss status --json`
emits under its own "health" key and the two surfaces can be diffed on one
machine. A test pins it by equality against toJson() rather than by spotting
fields, and a future field on the report can no longer collide with "available".

A drift test pins the registration and both supplier wirings. The failure it
guards is silent: the provider can compile, expose its tools and pass every one
of its own unit tests while never being registered, or while registered with no
suppliers behind it - in which case every call answers available=false on a
healthy host and the tools read as broken rather than absent. Source-text rather
than booting the registry, which would construct the policy engine, approval bus
and ledger and reach ~/.boss on the machine running the suite.

Both raised by @surfthewave-commits in review on risa-labs-inc#1364.
Vidhaankhare16 added a commit to Vidhaankhare16/BossConsole that referenced this pull request Sep 22, 2026
BOSS computes both halves of its own condition and exposed neither to a tool
caller. WorkspaceHealthReport is reachable only through SingleInstanceManager -
over the single-instance socket, from a CLI process outside the host - because
`boss doctor` was built for a human at a terminal (risa-labs-inc#418, risa-labs-inc#579). The performance
monitor publishes its snapshot to a settings panel and nowhere else. So the
agent inside the host, the party that has to decide whether to open another
browser tab or spawn another terminal, had no way to ask whether the host was
already degraded or near a ceiling; it found out by causing the failure.

Adds a host provider, boss-introspection, with get_workspace_health and
get_performance_metrics, both read-only by declaration rather than by the
mutating gate's fail-closed default.

The health tool returns WorkspaceHealthReport.toJson() - the same object
`boss status --json` emits - so the CLI and the MCP surface cannot drift into
two shapes for one concept, and scripts and agents match the same codes.

Counts and aggregates only. ResourceMetrics carries per-tab, per-terminal and
per-editor lists and CpuMetrics carries thread names; none of it travels. A
caller deciding whether it has headroom needs to know there are eleven browser
tabs, not which pages they are on.

A snapshot that does not exist yet reports sampling=false with no memory object
at all, never zeros, for the reason the health report separates unchecked from
"no findings": a caller reading 0 bytes used and concluding it has headroom is
worse off than one told the monitor has not reported.

Both sources are desktopMain and the provider is commonMain, so they arrive
through suppliers wired in main.kt the way WorkspaceMcpToolProvider.windowCreator
already is - which is also what lets the tests drive it with fakes.

The health payload is nested under "health" rather than flattened beside an
"available" flag, so the value is byte-identical to what `boss status --json`
emits under its own "health" key and the two surfaces can be diffed on one
machine. A test pins it by equality against toJson() rather than by spotting
fields, and a future field on the report can no longer collide with "available".

A drift test pins the registration and both supplier wirings. The failure it
guards is silent: the provider can compile, expose its tools and pass every one
of its own unit tests while never being registered, or while registered with no
suppliers behind it - in which case every call answers available=false on a
healthy host and the tools read as broken rather than absent. Source-text rather
than booting the registry, which would construct the policy engine, approval bus
and ledger and reach ~/.boss on the machine running the suite.

Both raised by @surfthewave-commits in review on risa-labs-inc#1364.
fluck-boss pushed a commit that referenced this pull request Sep 23, 2026
BOSS computes both halves of its own condition and exposed neither to a tool
caller. WorkspaceHealthReport is reachable only through SingleInstanceManager -
over the single-instance socket, from a CLI process outside the host - because
`boss doctor` was built for a human at a terminal (#418, #579). The performance
monitor publishes its snapshot to a settings panel and nowhere else. So the
agent inside the host, the party that has to decide whether to open another
browser tab or spawn another terminal, had no way to ask whether the host was
already degraded or near a ceiling; it found out by causing the failure.

Adds a host provider, boss-introspection, with get_workspace_health and
get_performance_metrics, both read-only by declaration rather than by the
mutating gate's fail-closed default.

The health tool returns WorkspaceHealthReport.toJson() - the same object
`boss status --json` emits - so the CLI and the MCP surface cannot drift into
two shapes for one concept, and scripts and agents match the same codes.

Counts and aggregates only. ResourceMetrics carries per-tab, per-terminal and
per-editor lists and CpuMetrics carries thread names; none of it travels. A
caller deciding whether it has headroom needs to know there are eleven browser
tabs, not which pages they are on.

A snapshot that does not exist yet reports sampling=false with no memory object
at all, never zeros, for the reason the health report separates unchecked from
"no findings": a caller reading 0 bytes used and concluding it has headroom is
worse off than one told the monitor has not reported.

Both sources are desktopMain and the provider is commonMain, so they arrive
through suppliers wired in main.kt the way WorkspaceMcpToolProvider.windowCreator
already is - which is also what lets the tests drive it with fakes.

The health payload is nested under "health" rather than flattened beside an
"available" flag, so the value is byte-identical to what `boss status --json`
emits under its own "health" key and the two surfaces can be diffed on one
machine. A test pins it by equality against toJson() rather than by spotting
fields, and a future field on the report can no longer collide with "available".

A drift test pins the registration and both supplier wirings. The failure it
guards is silent: the provider can compile, expose its tools and pass every one
of its own unit tests while never being registered, or while registered with no
suppliers behind it - in which case every call answers available=false on a
healthy host and the tools read as broken rather than absent. Source-text rather
than booting the registry, which would construct the policy engine, approval bus
and ledger and reach ~/.boss on the machine running the suite.

Both raised by @surfthewave-commits in review on #1364.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants