Repository navigation
Conversation
…-labs-inc#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 03ba543. 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.
…bs-inc#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 risa-labs-inc#667 * test: cover missing JxBrowser compile modules in release gate --------- Co-authored-by: Shivang <shivang.iitk@gmail.com>
…risa-labs-inc#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 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 * 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 risa-labs-inc#418 --------- Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
…reload (risa-labs-inc#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 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> * Stop the Shortcuts tester reporting working shortcuts as broken `ShortcutTestRunner.validateKeyName` kept its own hand-written set of valid key names - the fourth copy of the key vocabulary in the codebase, and the only one nothing tested. It had drifted, and step 4 of the tester turned that drift into a hard FAILED. Fifteen keys the interceptor dispatches were reported as "Unknown key name 'X' - won't match user input" on a shortcut that fires: - every function key, F1 through F12 - Home, End, PageUp, PageDown - the character spellings the fold has always accepted ("-", "=", "/" and the rest), which were reported with the confident and wrong advice "Key name should be 'Minus' not '-'" - the spellings Compose renders, both cold ("Back Slash", "Quote", "Page Up") and the macOS glyphs a running app produces The list is replaced by `isKnownKeyName`, which folds through `canonicalKeyName` against `KNOWN_KEY_NAMES` in the model - one vocabulary, beside the fold that already reconciles the other three. The verdict also softens to a note. "Will this key match" is not knowable from the name alone: `canonicalKeyName` folds an unknown name onto itself, and the keyboard has keys outside the interceptor's table (F13 and up, the keypad) that AWT still names. So an unrecognised name now says it is unrecognised alongside the result, and the test does not fail on it. Reporting a working shortcut as broken is worse than saying nothing, and that is the failure this PR is about. `ShortcutTesterKeyNamesTest` pins the vocabulary in both directions: every key the interceptor names is recognised, and nothing is claimed as known that no key produces. The second half is what stops the set being padded into passing the first. Every shipped preset binding is covered, and a name nothing can produce is still called out, so removing the false failures did not remove the check. Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt. Mutation-checked - restoring the two omissions the old list had (the function keys, and Home/End/PageUp/PageDown) fails the test and names all sixteen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review: keep a real verdict for a key stored as a raw key code Self-review of the change above found that softening step 4 to a note went one case too far. A key stored as a packed `Key.keyCode` is the one thing that IS decidable from the name alone, and it is decidably broken: no matcher compares against a number, so the binding fires on neither path. That is what every rebind made in the Shortcuts screen wrote before #329, which makes it the population most likely to be on this screen in the first place - and they would have been shown a green row with a parenthetical note. It keeps FAILED, with advice that holds either way: a keymap carrying a raw key code is worth re-recording whether or not the fold currently resolves it. Also drops a guarantee two comments claimed and the tests do not give. The test drives the interceptor's table through a hand-written list of key codes, so a key added to that table alone is checked by nothing. Reading the `when` itself would need reflection over a private table; saying so is better than asserting an invariant that is not enforced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * #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. - `Key…
…-labs-inc#679) * feat(import): Bitwarden JSON and KeePass XML password importers The CSV path already reads Bitwarden/1Password/LastPass/Chrome/Safari exports via the header aliases. This adds the two common export formats it cannot read: - BitwardenJsonParser: the unencrypted Bitwarden JSON export (the default Export vault offers). Reads login items only, resolves the website from the first URI or the item name, and returns nothing for an encrypted export rather than ciphertext masquerading as credentials. - KeePassXmlParser: the KeePass 2.x plaintext XML export. Gets two format traps right: an entry's <History> is not imported as extra credentials, and Recycle Bin entries are skipped. Parses untrusted XML with DTDs and external entities disabled (XXE hardening). Both are wired into ImportFileReader's content sniffing, which now routes KeePass XML and Bitwarden JSON ahead of the HTML/CSV paths on their unambiguous markers. Both use the low-level JSON/DOM APIs the host already has, so there are no new dependencies and no serializable models to drift. Neither logs a value. * fix(import): read Bitwarden passwords verbatim, wire json/xml picker, anchor KeePass sniff Addresses the review on risa-labs-inc#679: 1. BitwardenJsonParser read the password through the trimming string() helper, so " pa ss " lost its edge whitespace and an all-whitespace password was dropped as if absent. The password is now read verbatim; labels stay trimmed. 2. The import file picker only offered csv/html/htm, so a Bitwarden .json or KeePass .xml export could not be selected. It now offers json and xml too. 3. looksLikeKeePass searched the whole text, so a CSV whose field merely contained "<KeePassFile" was misrouted to the XML parser and rejected. Routing now anchors on the document root element (past an optional BOM, XML declaration and comments), so a marker inside CSV data no longer misroutes. Regressions added for each: verbatim/all-whitespace passwords, a no-declaration KeePass root, and a CSV/reader that carries the marker in a field. * fix(import): preserve exclusions and validate manager previews --------- Co-authored-by: Shivang <shivang.iitk@gmail.com>
* fix(filesystem): report failed file mutations * fix(filesystem): harden non-recursive deletion * test(filesystem): harden mutation regression coverage --------- Co-authored-by: ayush <workside@gamil.com> Co-authored-by: Shivang <shivang.iitk@gmail.com>
…a-labs-inc#696) (risa-labs-inc#697) * fix(updater): stop the update prompt stacking over another modal (risa-labs-inc#696) On the heavyweight-overlay path (Windows) every modal is a separate always-on-top window. The app-update prompt is the one dialog that appears on its own timer rather than in response to the user, so it could pop up over a dialog the user was filling in, or another dialog could open over it. Two always-on-top heavyweight modal windows stacked is the state risa-labs-inc#696 reports as leaving BOSS unusable. HeavyweightModalRegistry counts the open heavyweight modals (UI-thread DisposableEffect in HeavyweightModal, mirroring the existing popup counter). The update prompt now yields to any other heavyweight modal in either direction - it does not open over one, and it steps aside when one opens over it - reappearing once that modal closes. The banner stays up meanwhile. The count is always 0 on the lightweight path, where Compose stacks dialogs correctly on its own, so that path is unchanged. shouldShowUpdateDialog is a pure gate (subtracting the prompt's own window out of the count so it does not hide itself), pinned by UpdateDialogGateTest across the open / stay / step-aside / return sequence. * fix(updater): pin modal registration ordering and test lifecycle --------- Co-authored-by: Shivang <shivang.iitk@gmail.com>
…dow closes (risa-labs-inc#694) * Respect horizontal scrollers and commit trackpad navigation on release * Fix swipe cancellation, observer lifecycle and terminal delivery * Publish prior contact termination for home swipe attribution * fix(kernel): require verified caller identity for the run-configuration bridge (#634) * fix(overlays): honor RTL and fractional placement (#477) * 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> * 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(db): close signed-in crypto access and verify visibility boundaries * test(db): exercise repeated key rotation and exact anonymous ACLs * test(db): grant fixture role membership on Supabase Postgres * test(db): inline rotation source for isolated pgTAP mounts * test(db): keep generated rotation SQL out of source control * fix(db): reserve identity-taking store mutators for the edge service * fix(db): serialize rotations without requiring direct Vault writes * fix(db): address review gaps in system-org and rotation coverage * test(db): verify core secrets across hex and broker-free rotations * test(db): execute standing audit and missing-signature regression * fix(db): enforce revoke postconditions and harden rotation verification * test(db): enable recovery metadata in the authenticated fixture * test(db): use the schema-supported authenticator type * Address remaining database review quality findings * Fix invoker view audit boolean parsing and test timestamp bounds * Rotate the TOTP envelope, exempt extension routines, and close the repo/prod drift Addresses the handoff's database-integration item and the substantiated findings from the current-head review. Everything below was executed against the PR's own Supabase preview branch, not asserted from reading. **The blocker: rotation refused to run once #417 landed.** The guard was a blanket "TOTP is installed, refuse everything". It is now an adapter. The stored form is 'v1:' || encrypt_text(...) - same cipher, same key, only the framing differs - so the column map gained an envelope prefix and rotates like the rest once the prefix is stripped and re-applied. Three things this needed that reading the diff would not have shown: * #417's trigger RAISES on a v1: value ('TOTP input must be plaintext, not a storage envelope'), so the re-encryption update is rejected outright. The reviewed trigger is now disabled for the update and restored after, inside the same transaction. * safe_decrypt_recovery_codes returns jsonb, not text, so verification through the real read path needed a cast. Both fingerprints now go through each column's OWN application read path rather than decrypt_text, which also removes the step-1/step-5 asymmetry raised as U5. * a step 0 pre-check reports rows that are already unreadable, instead of letting a safe_decrypt_* NULL surface at the end as "a row was missed". The rejection contract is preserved, not removed: an unknown envelope version, an unmapped safe_decrypt_* wrapper, or an unreviewed BEFORE trigger on a mapped table each still refuse. All three are now regression-tested, along with the recovery-code column that was previously never exercised (its fixture was vacuous - create_secret writes no secret_metadata row without p_twofa_enabled). Verified: three consecutive rotations, TOTP/recovery/password all intact, v1 envelope preserved, trigger re-enabled and still enforcing its own contract. **CREATE EXTENSION was impossible.** The fail-closed guard aborted the first routine of any extension installed into public: `create extension pgtap` failed with "Anonymous EXECUTE remains on public.pg_version()". A guard that forces operators to disable it in order to install an extension is a guard that ends up disabled. Extension-owned routines are now exempted with a warning, and the exposure stays visible as advisory CHECK 1x rather than vanishing - it reports pgtap (1079) on the preview. The sweep skips them too: pgcrypto-style helpers are called from column DEFAULTs and CHECK constraints, which are evaluated with the DML role's privileges. No extension owns a function in public on a clean database, so no deployed behaviour changes. **The repo was weaker than production.** find_user_by_email, get_session_status and update_api_key_last_used were locked down on the live project on 2026-09-08 but never captured as a migration, so a fresh deploy re-opened them. find_user_by_email is a user-enumeration oracle over auth.users and signup is open. The standing audit caught this itself on the preview - CHECK 2 named it - which is the drift that check exists for. Both remaining callers use SUPABASE_SERVICE_ROLE_KEY, verified on this branch, so service_role keeps EXECUTE and neither path changes. Also: the guard no longer hard-fails on a database without the Supabase roles (a plain restore), and the vault fixtures no longer assume the key is absent (U3) - both suites failed outright on any database that already had one. Retained-key assertions measure this run's delta instead of an absolute count. Suites on the preview: rotation 16/16, db access audit, and visibility 54/54 all clean; the standing audit reports no failing gated check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Exercise the anonymous read path, not just the grant The keep-list assertions only checked has_function_privilege for the seven allowlisted signatures. That proves the grant, not the path: an RLS policy expression is evaluated as the QUERYING role, so revoking a policy helper turns an anonymous SELECT into 'permission denied for function ...' rather than an empty result, and no privilege assertion can see it. The seeding is the part that matters. A policy expression runs PER ROW, so on an empty table it is never evaluated - and plugins and user_roles are both empty on a fresh database, which made the first version of this probe pass no matter what had been revoked. Caught by trying to make it fail. Sensitivity is stated from measurement, not assumption: with a row present, revoking can_view_plugin_row from anon does fail the probe and revoking authorize does not, because permissive policies are ORed and short-circuit. A helper in a policy that never has to be evaluated cannot be detected this way by any test, so the comment says so rather than implying broader coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the gated audit checks and the test generator provable Both halves of this are the same failure: a suite that is green while proving nothing. I hit that twice writing the previous commits - a recovery-code fixture over a secret_metadata row create_secret never wrote, and an anonymous-read probe over an empty table whose RLS policy therefore never evaluated - so these are not hypothetical. Finding 6. The four CI-GATED checks (1, 1b, 4, 5) only ever asserted HEALTHY, while every advisory check already had a fixture-based detection test. An inverted `not exists`, a wrong role literal or a typo in the evtenabled test would have left the gate permanently green. Each is now broken deliberately and asserted to report it: a routine granted to anon AFTER creation (which is how one gets past the event trigger), a revoked deliberate anonymous grant, a disabled event trigger, and a crypto routine exposed to authenticated. They have teeth by construction - the assertion is that the finding appears, so a broken audit query fails them. Finding 8. The generator gained --check, which regenerates in memory and compares without writing, and orphan removal for a generated suite whose .sql.in has been renamed or deleted - that file otherwise keeps being executed forever with stale content. Orphans are identified by a provenance banner rather than a hard-coded list, so a hand-written suite is never touched; verified that explicit_anon_and_org_visibility_test.sql is untouched. Each guard was proven by making it fire: a hand edit and a planted orphan are both reported by --check, and generate removes the orphan. CI now runs generate and --check as their own step, so a preparation failure is visible in the log instead of being attributed to the test run. Preview: rotation 16/16, db access audit 15/15, visibility 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Announce every project change, not only the plugin-initiated ones Panels could come up empty and stay empty, and the reason was that the startup restore told no one. ProjectChangeEvent was published from ProjectDataProviderImpl.selectProject, which is the path a PLUGIN takes. The two callers that matter most bypass it: WorkspaceApplier.applyWorkspace calls windowProjectState.selectProject directly when it restores the workspace, and BossTopBar's picker goes through selectProjectInWindow. Neither published anything. The ordering that produced the empty panel: DefaultPlugin is constructed in a DisposableEffect keyed on registries and window state, not on selectedProject, and it kicks off the plugin scan asynchronously while BossAppScaffold composes and the sidebar builds panels immediately. The restore meanwhile waits on WorkspaceManager's sequential Dispatchers.IO JSON reads. When those reads are slow the panel is built first, reads the "" that WindowProjectState seeds itself with, and renders its no-project state. The restore then lands, silently, and nothing remounts the cached component. Publishing from the state itself rather than from one caller catches all three. The bus is MutableSharedFlow(replay = 0), so a publish that never happens cannot be recovered by a later subscriber - which is why this could not stay per-caller. The publish is removed from selectProject rather than added alongside the collector; keeping both would double-fire on the plugin path. previousPath is seeded from the current value so the StateFlow's replay of it is not announced as a change. That seed is "" at startup, and telling every plugin the project just became "" moments before the real restore arrives is precisely the clear-yourself signal worth avoiding. One collector per window: projectDataProvider is `by lazy` on DefaultPlugin, which is per-window, so this cannot stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Publish from the state's own callback, not from a lazily-built provider The review's finding 1: observing `selectedProject` from `ProjectDataProviderImpl` put the fix behind two `by lazy` initializers. `projectDataProvider` and `applicationEventBus` are both lazy on `DefaultPlugin`, and the second is the only host path that creates the bus at all. So the collector started only if some plugin touched both before the workspace JSON reads finished - and "the reads are slow" is the premise of the bug. Touched after the restore, the collector seeds to the restored path and announces nothing; never touched, no caller is announced at all. The announcement now hangs off the `ProjectSelectionCallback` that `WindowProjectStateRegistry` installs when it builds the window's state, which `BossAppState` does eagerly in composition. `WindowProjectState.selectProject` is the sole mutator of the selection and invokes that callback synchronously, so every caller is covered with no coroutine, no scope and nothing lazy in the path. Being synchronous also makes `previousProjectPath` a faithful history rather than best-effort: there is no StateFlow conflation to collapse A -> B -> C into A -> C. The two registry entry points carried a copy of the wiring each; they now share one `newState`, so the announcement cannot be installed on some windows and not others. Finding 1's third mitigation, which is a hole of its own: `publishSystemEvent` was a no-op whenever the bus had not been created, so on a build where no installed plugin had touched `applicationEventBus` yet, no host event existed - not `ProjectChangeEvent`, not `AuthEvent`, not `TabEvent` - and with `replay = 0` none could be recovered. It now creates the bus instead of dropping. `getInstance` also re-checks the registry on every call rather than only at creation, so an instance that exists while the registry is empty can no longer strand the host's publisher. Findings 2 and 3: `ProjectDataProviderImpl` implements `DisposableProvider` and `DefaultPlugin.dispose()` releases it through a named delegate, alongside `logDataProviderDelegate` and `gitDataProviderDelegate`; its scope gets a `SupervisorJob`, matching `pluginScope`. Tests: `ProjectChangeAnnouncementTest` (6) pins a direct `selectProject` being announced, the previous-path chain, the seeding rule, the same-path suppression, both registry entry points, and the bus being created rather than dropping the event. Full `:composeApp:desktopTest` green: 3695 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 2: honest bus wording, atomic previousPath, three more tests Review (1/2) and (2/2). 1. `publishSystemEvent` does not save the event it creates the bus for. Correct - no registered publisher means nobody holds the bus, so it has no subscribers and that first event still reaches no one. The comment, the test name and the PR body all claimed otherwise. Reworded to what it actually buys: the host stops being permanently silent while it waits for a plugin to touch `applicationEventBus`. 3. `previousPath` was check-then-act on a plain field, and `ProjectDataServiceBridge.selectProject` is a suspend gRPC handler with no hop to Main (verified: `KernelBootstrap` registers it, the bridge calls `provider.selectProject` directly). Now an `AtomicReference.getAndSet`, so the read and the write are one operation. 5. The registry read-modify-write moved back inside `synchronized(this)`. Both fields are in fact `@Volatile` in boss-plugin-api 1.0.87 (checked the pinned jar), but the lock is uncontended after the first call and removes the question. The bus-before-publisher ordering is now written down, including why a publish landing in that window loses nothing. 6. `initialPath` KDoc no longer describes a production scenario it cannot reach; it is documented as defensive and test-reachable, and defaulted to "". 7. Three tests added: - a plugin-initiated selection is announced exactly once (catches both a silent regression to zero and an accidentally re-added publish); - the registry test now asserts the recent-projects half of the callback ran - deleting it from `newState` previously left every test green; - `dispose()` stops the recent-projects collector, which is the whole reason `projectDataProviderDelegate` became a named lazy. `Dispatchers.setMain(UnconfinedTestDispatcher())` for the class: the provider collects on `Dispatchers.Main`, which has no implementation in a plain test JVM, so its collector silently never ran. Unconfined also makes the dispose test deterministic rather than a sleep. Smaller: `SupervisorJob` comment describes it as future-proofing (there is one collector); `systemEventBusScope` is `by lazy`; the KERNEL-mode consequence of disposing the provider is documented on `dispose()`. Not taken: `replay = 1` for `ProjectChangeEvent`. It would close the mirror race, but it changes `events()` semantics for every subscriber and every event type on a bus whose consumers are out of tree. The body now says the race is narrowed rather than closed, and names the api-side `selectedProject` StateFlow as what closes it. `:composeApp:desktopTest` 3697 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 3: unconditional announcement, ordered publish, drop dead register Review 3. 2. AGENTS.md: a bullet in the canonical "what a third-party plugin can observe" paragraph, since this widens *when* a filesystem path reaches every installed plugin. 3. The `expect` KDoc still said "a no-op if the bus has not been created yet". Only the `actual` had been corrected. Fixed, and it now states the part that IS still best-effort: replay = 0 means an unsubscribed event is gone either way. 4. The announcement sat downstream of `ProjectState.updateRecentProjects`, so a throw there skipped it AND left `previousPath` stale - every later selection reporting a previous path one step behind, silently, for the session. Now `try { recents } finally { announce }`: order preserved for anyone reading recents off the event, the announcement unconditional, the throw still propagating. 5. `getAndSet` closed the read-modify-write but not the publish order, so two concurrent selections could emit out of order. `previousPath` and the publish are now in one `synchronized` block (the AtomicReference is gone - the lock subsumes it). The KDoc says plainly what this still does not settle: `WindowProjectState` writes `_selectedProject.value` then calls the callback with no atomicity, so state and last event can still disagree. Closing that means locking upstream. 6. `ProjectState`'s async `loadRecentProjects()` assigns `_recentProjects.value` wholesale and could land mid-test, dropping a just-added path - only where `~/.boss/recent-projects.json` exists, so a developer-machine-only flake. A once-per-JVM settle in `@BeforeTest` puts that single file read before any test. The test paths stay deliberately non-existent, now with a comment saying why: the fire-and-forget saves are unordered, and `loadRecentProjects` reclaims entries whose directory is gone, so a real temp directory would leak where these self-heal. 7. `register` deleted. No production caller (`BossAppState:299` uses `getOrCreate`), and it overwrote an existing entry - handing back a state with a fresh announcer seeded to "" for a window that already had a project. `newState` stays; it is what keeps the wiring from drifting. 8. `@Volatile` dropped from `ApplicationEventBusImpl.instance` - every access is inside the lock now, and leaving it reads as if a fast path survives. Also: a test for the actual regression path, `applyWorkspace` announcing the project it restores, rather than a comment claiming to imitate it. Answering the grep you could not run: the only `projectChanges()` consumers in boss_plugins are the two fluck-agent panels (`FluckAgentViewModel`), and both assign `_bossProject.value` and re-sweep - idempotent on a repeat. Nothing used the repeat publish as a reload nudge. Recorded in the announcer comment. Item 1 was already in the PR body from round 2 ("The startup race is narrowed, not closed"), naming the mirror case and the api-side StateFlow that closes it. `:composeApp:desktopTest` 3698 tests / 368 classes, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 4: split the dispose out, stop tests writing the real ~/.boss Review 4. Items 1 and 3 were the before-merge ones. 1. Verified: ProjectDataServiceBridge.watchRecentProjects collects provider.recentProjects, a StateFlow, so cancelling the provider's scope on window close leaves that gRPC stream open and silently frozen for every out-of-process plugin. Taking the first option and SPLITTING the dispose out. Note this reverses what round 1 asked for, and the justification changed rather than the opinion: once the announcement moved to the registry callback, ProjectDataProviderImpl went back to being exactly what it was before this PR, so the leak is pre-existing and unrelated to the subject. Closing it properly means the bridge reading ProjectState directly instead of a per-window provider, which is its own change. The reasoning is on the class so it is not re-fixed by accident. 3. Verified and worse than described: MAX_RECENT_PROJECTS = 10, so test entries evict real ones from the developer's picker and no cleanup restores them. systemProperty("user.home", <build>/test-home) on the Test task. One run with it in place shows the existing suite was writing far more than recents to the real home - window-appearance-settings.json, keymap-settings.json, scrollbar-settings.json, recent-browser-pages.json, dashboard-stats.json and a ~/BossProjects/ directory all land in test-home now. The settle is gone with it: no recent-projects.json in test-home means the wholesale reload never happens. 2. publishSystemEvent now refuses the bus != null && systemPublisher == null state instead of falling through into a bus that may not be the registry's, with one warning rather than one per event. The comment at the fallback is true as written. 4. Both stale comments fixed (one construction path, not two). 5. hostProjectCallback(updateRecents, announcer) extracted so the try/finally claim is testable - ProjectState is an object, so a hard-coded call cannot be made to fail. 6. The publish-inside-the-lock trade is spelled out in the KDoc: what it buys (ordering), what it costs (an inline subscriber runs holding the lock inside selectProject), and that the contended path is untested. 7. The serial-execution dependency is recorded in the test class KDoc. Plus: multi-window isolation, and a test that makes the recents update throw. One thing this round caused and fixed: removing dispose() left two ProjectDataProviderImpl collectors alive on the test dispatcher. On a TestCoroutineScheduler a live coroutine is something every later runTest waits on, and the git classes timed out at 60s in full-suite runs while passing in isolation. Dispatchers.setMain(Dispatchers.Unconfined) rather than UnconfinedTestDispatcher(): same inline behaviour, not enrolled in a scheduler anyone else observes. It is also a real argument for the follow-up - without dispose() the provider cannot be constructed in a test without leaking. :composeApp:desktopTest 3699 tests / 368 classes, 0 failures. detekt ktlintCheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 5: reset test-home per run, stop getInstance stealing a publisher Review 5. 1. Confirmed reproducibly - build/test-home/.boss/recent-projects.json was sitting there from the previous run. doFirst now deleteRecursively() before mkdirs(), so the home is fresh per run rather than merely private. Without it the redirect only helped the first run and ProjectState.init's load reintroduced the same race from the second. Verified by running the full suite twice back to back. 2. Right, and sharper than it looks: the guard tested `bus` but wrote both fields, and `systemPublisher != null && bus == null` is exactly what ProjectChangeAnnouncementTest, BrowserAnalyticsEmissionTest and BossTabsComponentMoveTest install. Guard now tests both, so getInstance cannot take a publisher away from whoever set it. 3. Test added for the warn-and-drop branch, asserting the DROP rather than the warning - partialRegistryWarned is a one-shot process global, so "it warns" is unassertable after the first test to trip it. Uses a stub bus so the half-registry state is real. 4. The re-entrancy hazard is now documented as SAFE and why (synchronized is reentrant, previousPath advances before the publish), so it is not "fixed" later. The lock-widens-a-UI-hang-to-a-cross-thread-one point is in the same block. 5. Taken, including the optional ones: - hostProjectCallback keeps both failures (addSuppressed) instead of letting finally discard the first. Needs @Suppress("TooGenericExceptionCaught") - catching Throwable is the contract, and both are rethrown. - ProjectDataProviderImpl takes an injectable dispatcher (default Dispatchers.Main). This removes the global setMain AND the round-4 collector leak at the source rather than working around them, without reopening the DisposableProvider split. - Three more pins: a null window state announces nothing, applyWorkspace (restoreProject = false) announces nothing, unregister + getOrCreate restarts the chain at "". Not taken, with reasons: jvmArgumentProviders (the current form works, is config-cache-serialisable and passed CI on three OSes); widening the user.home redirect to plugin-path-utils (its test only mkdirs a directory every BOSS user already has). There is no CHANGELOG in this repo, so the release-notes line for the same-path change is an action item at release time, not a file change. Flagged in the body. Two consecutive full runs: 3703 tests / 368 classes, 0 failures both times. detekt ktlintCheck clean - it caught two TooGenericExceptionCaught and a long line in this round's own code first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address project announcement review feedback * fix(plugin-store): restrict plugin_downloads RLS to the server-side pipeline (#488) * Batch validation: promote latest reviewed dev changes to main (#463) * Fix #30: Resolve duplicate context menu label collisions * Cleanup: Remove dead code for #91 and #93 * chore: Sanitize SubmitResult.Error at construction rather than at one render site * Fix panel component lifecycle disposal * fixes * Guard panel lifecycle destruction failures * fix(logging): sanitize secret RPC failures and crash-report errors Three related gaps in what gets logged and shown when something fails: - SecretService.kt had no BossLogger in any of its ten catch blocks (#145). When the organisation migration broke all four secret RPCs (#144), the only WARN in the log came from the calling plugin - the code that actually failed said nothing. Every catch now logs via logger.warn(LogCategory.NETWORK, ...), reusing the already-sanitized failure for both the log and the returned Result rather than sanitizing twice. RoleService and RoleCreationService were checked and already log correctly - SecretService was the only silent one. - LogSanitizer.sanitizeExceptionMessage redacted a hostname only when it appeared inside a URL - a bare hostname (exactly what UnknownHostException.getMessage() produces, i.e. every DNS failure, and what a proxy-connect failure looks like) passed through untouched (#109). Added a narrow hostname pattern: lowercase-only labels ending in a short explicit TLD/`.internal`/`.local` list, with a negative lookahead so a package path that happens to end in a real TLD word mid-FQN (kotlinx.coroutines.internal.ScopeCoroutine, kotlinx.io.EOFException) is not mistaken for one - caught by the existing realistic-stack-trace test after the first version of the pattern redacted a live Kotlin package name out of it. - CrashReportService.SubmitResult.Error held a raw exception string, sanitized at exactly one render call site in CrashReportDialog (#110). The constructor is now private; SubmitResult.Error.of(...) is the only way to build one, and it sanitizes before the raw string can reach .message - a property of the type now, not something every future consumer (a copy button, a toast, a log line) has to remember on its own. Both construction sites (the service's two catch blocks, and the one in CrashReportDialog the issue names directly) now route through it. Tests: new coverage for the ten now-logging SecretService paths (via SupabaseWiringTest's existing sanitization-wiring guard, updated to recognize the "sanitize once into a local, reuse for log and return" shape SupabaseDataProviderImpl already used), six new LogSanitizerTest cases for the hostname fix (including the FQN/version-number false-positive guards), and a new CrashReportServiceTest for the sanitize-at-construction guarantee. * test(crash): extract construction tests from #404 Selected unchanged test file from Antriksh1984 original commit 99d2a5228023c541e825fe2714619c8228140369 (PR #404). Maintainer extraction only; subsequent API/scope adaptations are recorded separately. * chore(ipc): clarify advisory menu ID scope and fix formatting * chore(ui): finish obsolete toolbar comment cleanup * test(crash): adapt consolidated coverage and guard factory visibility Maintainer consolidation: use #311 companion invoke and a URL fixture instead of depending on #404 hostname redaction. Add private constructor/copy bytecode assertions. Preserve original contributor tests and implementation credit separately. * refactor(crash): consolidate construction sanitization in #311 Maintainer scope change: remove only the duplicate #110 implementation, retaining Antriksh1984 original #109/#145 work and commit history. Standalone construction tests are retained with attribution in #311; its Aditya8369 implementation supersedes Error.of. Keep render-time sanitization here until #311 merges and describe the remaining hostname coverage conservatively. * 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> * 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> * 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 sp…
…nc#710) * Respect horizontal scrollers and commit trackpad navigation on release * Fix swipe cancellation, observer lifecycle and terminal delivery * Publish prior contact termination for home swipe attribution * fix(kernel): require verified caller identity for the run-configuration bridge (#634) * fix(overlays): honor RTL and fractional placement (#477) * 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> * 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(db): close signed-in crypto access and verify visibility boundaries * test(db): exercise repeated key rotation and exact anonymous ACLs * test(db): grant fixture role membership on Supabase Postgres * test(db): inline rotation source for isolated pgTAP mounts * test(db): keep generated rotation SQL out of source control * fix(db): reserve identity-taking store mutators for the edge service * fix(db): serialize rotations without requiring direct Vault writes * fix(db): address review gaps in system-org and rotation coverage * test(db): verify core secrets across hex and broker-free rotations * test(db): execute standing audit and missing-signature regression * fix(db): enforce revoke postconditions and harden rotation verification * test(db): enable recovery metadata in the authenticated fixture * test(db): use the schema-supported authenticator type * Address remaining database review quality findings * Fix invoker view audit boolean parsing and test timestamp bounds * Rotate the TOTP envelope, exempt extension routines, and close the repo/prod drift Addresses the handoff's database-integration item and the substantiated findings from the current-head review. Everything below was executed against the PR's own Supabase preview branch, not asserted from reading. **The blocker: rotation refused to run once #417 landed.** The guard was a blanket "TOTP is installed, refuse everything". It is now an adapter. The stored form is 'v1:' || encrypt_text(...) - same cipher, same key, only the framing differs - so the column map gained an envelope prefix and rotates like the rest once the prefix is stripped and re-applied. Three things this needed that reading the diff would not have shown: * #417's trigger RAISES on a v1: value ('TOTP input must be plaintext, not a storage envelope'), so the re-encryption update is rejected outright. The reviewed trigger is now disabled for the update and restored after, inside the same transaction. * safe_decrypt_recovery_codes returns jsonb, not text, so verification through the real read path needed a cast. Both fingerprints now go through each column's OWN application read path rather than decrypt_text, which also removes the step-1/step-5 asymmetry raised as U5. * a step 0 pre-check reports rows that are already unreadable, instead of letting a safe_decrypt_* NULL surface at the end as "a row was missed". The rejection contract is preserved, not removed: an unknown envelope version, an unmapped safe_decrypt_* wrapper, or an unreviewed BEFORE trigger on a mapped table each still refuse. All three are now regression-tested, along with the recovery-code column that was previously never exercised (its fixture was vacuous - create_secret writes no secret_metadata row without p_twofa_enabled). Verified: three consecutive rotations, TOTP/recovery/password all intact, v1 envelope preserved, trigger re-enabled and still enforcing its own contract. **CREATE EXTENSION was impossible.** The fail-closed guard aborted the first routine of any extension installed into public: `create extension pgtap` failed with "Anonymous EXECUTE remains on public.pg_version()". A guard that forces operators to disable it in order to install an extension is a guard that ends up disabled. Extension-owned routines are now exempted with a warning, and the exposure stays visible as advisory CHECK 1x rather than vanishing - it reports pgtap (1079) on the preview. The sweep skips them too: pgcrypto-style helpers are called from column DEFAULTs and CHECK constraints, which are evaluated with the DML role's privileges. No extension owns a function in public on a clean database, so no deployed behaviour changes. **The repo was weaker than production.** find_user_by_email, get_session_status and update_api_key_last_used were locked down on the live project on 2026-09-08 but never captured as a migration, so a fresh deploy re-opened them. find_user_by_email is a user-enumeration oracle over auth.users and signup is open. The standing audit caught this itself on the preview - CHECK 2 named it - which is the drift that check exists for. Both remaining callers use SUPABASE_SERVICE_ROLE_KEY, verified on this branch, so service_role keeps EXECUTE and neither path changes. Also: the guard no longer hard-fails on a database without the Supabase roles (a plain restore), and the vault fixtures no longer assume the key is absent (U3) - both suites failed outright on any database that already had one. Retained-key assertions measure this run's delta instead of an absolute count. Suites on the preview: rotation 16/16, db access audit, and visibility 54/54 all clean; the standing audit reports no failing gated check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Exercise the anonymous read path, not just the grant The keep-list assertions only checked has_function_privilege for the seven allowlisted signatures. That proves the grant, not the path: an RLS policy expression is evaluated as the QUERYING role, so revoking a policy helper turns an anonymous SELECT into 'permission denied for function ...' rather than an empty result, and no privilege assertion can see it. The seeding is the part that matters. A policy expression runs PER ROW, so on an empty table it is never evaluated - and plugins and user_roles are both empty on a fresh database, which made the first version of this probe pass no matter what had been revoked. Caught by trying to make it fail. Sensitivity is stated from measurement, not assumption: with a row present, revoking can_view_plugin_row from anon does fail the probe and revoking authorize does not, because permissive policies are ORed and short-circuit. A helper in a policy that never has to be evaluated cannot be detected this way by any test, so the comment says so rather than implying broader coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the gated audit checks and the test generator provable Both halves of this are the same failure: a suite that is green while proving nothing. I hit that twice writing the previous commits - a recovery-code fixture over a secret_metadata row create_secret never wrote, and an anonymous-read probe over an empty table whose RLS policy therefore never evaluated - so these are not hypothetical. Finding 6. The four CI-GATED checks (1, 1b, 4, 5) only ever asserted HEALTHY, while every advisory check already had a fixture-based detection test. An inverted `not exists`, a wrong role literal or a typo in the evtenabled test would have left the gate permanently green. Each is now broken deliberately and asserted to report it: a routine granted to anon AFTER creation (which is how one gets past the event trigger), a revoked deliberate anonymous grant, a disabled event trigger, and a crypto routine exposed to authenticated. They have teeth by construction - the assertion is that the finding appears, so a broken audit query fails them. Finding 8. The generator gained --check, which regenerates in memory and compares without writing, and orphan removal for a generated suite whose .sql.in has been renamed or deleted - that file otherwise keeps being executed forever with stale content. Orphans are identified by a provenance banner rather than a hard-coded list, so a hand-written suite is never touched; verified that explicit_anon_and_org_visibility_test.sql is untouched. Each guard was proven by making it fire: a hand edit and a planted orphan are both reported by --check, and generate removes the orphan. CI now runs generate and --check as their own step, so a preparation failure is visible in the log instead of being attributed to the test run. Preview: rotation 16/16, db access audit 15/15, visibility 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Announce every project change, not only the plugin-initiated ones Panels could come up empty and stay empty, and the reason was that the startup restore told no one. ProjectChangeEvent was published from ProjectDataProviderImpl.selectProject, which is the path a PLUGIN takes. The two callers that matter most bypass it: WorkspaceApplier.applyWorkspace calls windowProjectState.selectProject directly when it restores the workspace, and BossTopBar's picker goes through selectProjectInWindow. Neither published anything. The ordering that produced the empty panel: DefaultPlugin is constructed in a DisposableEffect keyed on registries and window state, not on selectedProject, and it kicks off the plugin scan asynchronously while BossAppScaffold composes and the sidebar builds panels immediately. The restore meanwhile waits on WorkspaceManager's sequential Dispatchers.IO JSON reads. When those reads are slow the panel is built first, reads the "" that WindowProjectState seeds itself with, and renders its no-project state. The restore then lands, silently, and nothing remounts the cached component. Publishing from the state itself rather than from one caller catches all three. The bus is MutableSharedFlow(replay = 0), so a publish that never happens cannot be recovered by a later subscriber - which is why this could not stay per-caller. The publish is removed from selectProject rather than added alongside the collector; keeping both would double-fire on the plugin path. previousPath is seeded from the current value so the StateFlow's replay of it is not announced as a change. That seed is "" at startup, and telling every plugin the project just became "" moments before the real restore arrives is precisely the clear-yourself signal worth avoiding. One collector per window: projectDataProvider is `by lazy` on DefaultPlugin, which is per-window, so this cannot stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Publish from the state's own callback, not from a lazily-built provider The review's finding 1: observing `selectedProject` from `ProjectDataProviderImpl` put the fix behind two `by lazy` initializers. `projectDataProvider` and `applicationEventBus` are both lazy on `DefaultPlugin`, and the second is the only host path that creates the bus at all. So the collector started only if some plugin touched both before the workspace JSON reads finished - and "the reads are slow" is the premise of the bug. Touched after the restore, the collector seeds to the restored path and announces nothing; never touched, no caller is announced at all. The announcement now hangs off the `ProjectSelectionCallback` that `WindowProjectStateRegistry` installs when it builds the window's state, which `BossAppState` does eagerly in composition. `WindowProjectState.selectProject` is the sole mutator of the selection and invokes that callback synchronously, so every caller is covered with no coroutine, no scope and nothing lazy in the path. Being synchronous also makes `previousProjectPath` a faithful history rather than best-effort: there is no StateFlow conflation to collapse A -> B -> C into A -> C. The two registry entry points carried a copy of the wiring each; they now share one `newState`, so the announcement cannot be installed on some windows and not others. Finding 1's third mitigation, which is a hole of its own: `publishSystemEvent` was a no-op whenever the bus had not been created, so on a build where no installed plugin had touched `applicationEventBus` yet, no host event existed - not `ProjectChangeEvent`, not `AuthEvent`, not `TabEvent` - and with `replay = 0` none could be recovered. It now creates the bus instead of dropping. `getInstance` also re-checks the registry on every call rather than only at creation, so an instance that exists while the registry is empty can no longer strand the host's publisher. Findings 2 and 3: `ProjectDataProviderImpl` implements `DisposableProvider` and `DefaultPlugin.dispose()` releases it through a named delegate, alongside `logDataProviderDelegate` and `gitDataProviderDelegate`; its scope gets a `SupervisorJob`, matching `pluginScope`. Tests: `ProjectChangeAnnouncementTest` (6) pins a direct `selectProject` being announced, the previous-path chain, the seeding rule, the same-path suppression, both registry entry points, and the bus being created rather than dropping the event. Full `:composeApp:desktopTest` green: 3695 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 2: honest bus wording, atomic previousPath, three more tests Review (1/2) and (2/2). 1. `publishSystemEvent` does not save the event it creates the bus for. Correct - no registered publisher means nobody holds the bus, so it has no subscribers and that first event still reaches no one. The comment, the test name and the PR body all claimed otherwise. Reworded to what it actually buys: the host stops being permanently silent while it waits for a plugin to touch `applicationEventBus`. 3. `previousPath` was check-then-act on a plain field, and `ProjectDataServiceBridge.selectProject` is a suspend gRPC handler with no hop to Main (verified: `KernelBootstrap` registers it, the bridge calls `provider.selectProject` directly). Now an `AtomicReference.getAndSet`, so the read and the write are one operation. 5. The registry read-modify-write moved back inside `synchronized(this)`. Both fields are in fact `@Volatile` in boss-plugin-api 1.0.87 (checked the pinned jar), but the lock is uncontended after the first call and removes the question. The bus-before-publisher ordering is now written down, including why a publish landing in that window loses nothing. 6. `initialPath` KDoc no longer describes a production scenario it cannot reach; it is documented as defensive and test-reachable, and defaulted to "". 7. Three tests added: - a plugin-initiated selection is announced exactly once (catches both a silent regression to zero and an accidentally re-added publish); - the registry test now asserts the recent-projects half of the callback ran - deleting it from `newState` previously left every test green; - `dispose()` stops the recent-projects collector, which is the whole reason `projectDataProviderDelegate` became a named lazy. `Dispatchers.setMain(UnconfinedTestDispatcher())` for the class: the provider collects on `Dispatchers.Main`, which has no implementation in a plain test JVM, so its collector silently never ran. Unconfined also makes the dispose test deterministic rather than a sleep. Smaller: `SupervisorJob` comment describes it as future-proofing (there is one collector); `systemEventBusScope` is `by lazy`; the KERNEL-mode consequence of disposing the provider is documented on `dispose()`. Not taken: `replay = 1` for `ProjectChangeEvent`. It would close the mirror race, but it changes `events()` semantics for every subscriber and every event type on a bus whose consumers are out of tree. The body now says the race is narrowed rather than closed, and names the api-side `selectedProject` StateFlow as what closes it. `:composeApp:desktopTest` 3697 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 3: unconditional announcement, ordered publish, drop dead register Review 3. 2. AGENTS.md: a bullet in the canonical "what a third-party plugin can observe" paragraph, since this widens *when* a filesystem path reaches every installed plugin. 3. The `expect` KDoc still said "a no-op if the bus has not been created yet". Only the `actual` had been corrected. Fixed, and it now states the part that IS still best-effort: replay = 0 means an unsubscribed event is gone either way. 4. The announcement sat downstream of `ProjectState.updateRecentProjects`, so a throw there skipped it AND left `previousPath` stale - every later selection reporting a previous path one step behind, silently, for the session. Now `try { recents } finally { announce }`: order preserved for anyone reading recents off the event, the announcement unconditional, the throw still propagating. 5. `getAndSet` closed the read-modify-write but not the publish order, so two concurrent selections could emit out of order. `previousPath` and the publish are now in one `synchronized` block (the AtomicReference is gone - the lock subsumes it). The KDoc says plainly what this still does not settle: `WindowProjectState` writes `_selectedProject.value` then calls the callback with no atomicity, so state and last event can still disagree. Closing that means locking upstream. 6. `ProjectState`'s async `loadRecentProjects()` assigns `_recentProjects.value` wholesale and could land mid-test, dropping a just-added path - only where `~/.boss/recent-projects.json` exists, so a developer-machine-only flake. A once-per-JVM settle in `@BeforeTest` puts that single file read before any test. The test paths stay deliberately non-existent, now with a comment saying why: the fire-and-forget saves are unordered, and `loadRecentProjects` reclaims entries whose directory is gone, so a real temp directory would leak where these self-heal. 7. `register` deleted. No production caller (`BossAppState:299` uses `getOrCreate`), and it overwrote an existing entry - handing back a state with a fresh announcer seeded to "" for a window that already had a project. `newState` stays; it is what keeps the wiring from drifting. 8. `@Volatile` dropped from `ApplicationEventBusImpl.instance` - every access is inside the lock now, and leaving it reads as if a fast path survives. Also: a test for the actual regression path, `applyWorkspace` announcing the project it restores, rather than a comment claiming to imitate it. Answering the grep you could not run: the only `projectChanges()` consumers in boss_plugins are the two fluck-agent panels (`FluckAgentViewModel`), and both assign `_bossProject.value` and re-sweep - idempotent on a repeat. Nothing used the repeat publish as a reload nudge. Recorded in the announcer comment. Item 1 was already in the PR body from round 2 ("The startup race is narrowed, not closed"), naming the mirror case and the api-side StateFlow that closes it. `:composeApp:desktopTest` 3698 tests / 368 classes, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 4: split the dispose out, stop tests writing the real ~/.boss Review 4. Items 1 and 3 were the before-merge ones. 1. Verified: ProjectDataServiceBridge.watchRecentProjects collects provider.recentProjects, a StateFlow, so cancelling the provider's scope on window close leaves that gRPC stream open and silently frozen for every out-of-process plugin. Taking the first option and SPLITTING the dispose out. Note this reverses what round 1 asked for, and the justification changed rather than the opinion: once the announcement moved to the registry callback, ProjectDataProviderImpl went back to being exactly what it was before this PR, so the leak is pre-existing and unrelated to the subject. Closing it properly means the bridge reading ProjectState directly instead of a per-window provider, which is its own change. The reasoning is on the class so it is not re-fixed by accident. 3. Verified and worse than described: MAX_RECENT_PROJECTS = 10, so test entries evict real ones from the developer's picker and no cleanup restores them. systemProperty("user.home", <build>/test-home) on the Test task. One run with it in place shows the existing suite was writing far more than recents to the real home - window-appearance-settings.json, keymap-settings.json, scrollbar-settings.json, recent-browser-pages.json, dashboard-stats.json and a ~/BossProjects/ directory all land in test-home now. The settle is gone with it: no recent-projects.json in test-home means the wholesale reload never happens. 2. publishSystemEvent now refuses the bus != null && systemPublisher == null state instead of falling through into a bus that may not be the registry's, with one warning rather than one per event. The comment at the fallback is true as written. 4. Both stale comments fixed (one construction path, not two). 5. hostProjectCallback(updateRecents, announcer) extracted so the try/finally claim is testable - ProjectState is an object, so a hard-coded call cannot be made to fail. 6. The publish-inside-the-lock trade is spelled out in the KDoc: what it buys (ordering), what it costs (an inline subscriber runs holding the lock inside selectProject), and that the contended path is untested. 7. The serial-execution dependency is recorded in the test class KDoc. Plus: multi-window isolation, and a test that makes the recents update throw. One thing this round caused and fixed: removing dispose() left two ProjectDataProviderImpl collectors alive on the test dispatcher. On a TestCoroutineScheduler a live coroutine is something every later runTest waits on, and the git classes timed out at 60s in full-suite runs while passing in isolation. Dispatchers.setMain(Dispatchers.Unconfined) rather than UnconfinedTestDispatcher(): same inline behaviour, not enrolled in a scheduler anyone else observes. It is also a real argument for the follow-up - without dispose() the provider cannot be constructed in a test without leaking. :composeApp:desktopTest 3699 tests / 368 classes, 0 failures. detekt ktlintCheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 5: reset test-home per run, stop getInstance stealing a publisher Review 5. 1. Confirmed reproducibly - build/test-home/.boss/recent-projects.json was sitting there from the previous run. doFirst now deleteRecursively() before mkdirs(), so the home is fresh per run rather than merely private. Without it the redirect only helped the first run and ProjectState.init's load reintroduced the same race from the second. Verified by running the full suite twice back to back. 2. Right, and sharper than it looks: the guard tested `bus` but wrote both fields, and `systemPublisher != null && bus == null` is exactly what ProjectChangeAnnouncementTest, BrowserAnalyticsEmissionTest and BossTabsComponentMoveTest install. Guard now tests both, so getInstance cannot take a publisher away from whoever set it. 3. Test added for the warn-and-drop branch, asserting the DROP rather than the warning - partialRegistryWarned is a one-shot process global, so "it warns" is unassertable after the first test to trip it. Uses a stub bus so the half-registry state is real. 4. The re-entrancy hazard is now documented as SAFE and why (synchronized is reentrant, previousPath advances before the publish), so it is not "fixed" later. The lock-widens-a-UI-hang-to-a-cross-thread-one point is in the same block. 5. Taken, including the optional ones: - hostProjectCallback keeps both failures (addSuppressed) instead of letting finally discard the first. Needs @Suppress("TooGenericExceptionCaught") - catching Throwable is the contract, and both are rethrown. - ProjectDataProviderImpl takes an injectable dispatcher (default Dispatchers.Main). This removes the global setMain AND the round-4 collector leak at the source rather than working around them, without reopening the DisposableProvider split. - Three more pins: a null window state announces nothing, applyWorkspace (restoreProject = false) announces nothing, unregister + getOrCreate restarts the chain at "". Not taken, with reasons: jvmArgumentProviders (the current form works, is config-cache-serialisable and passed CI on three OSes); widening the user.home redirect to plugin-path-utils (its test only mkdirs a directory every BOSS user already has). There is no CHANGELOG in this repo, so the release-notes line for the same-path change is an action item at release time, not a file change. Flagged in the body. Two consecutive full runs: 3703 tests / 368 classes, 0 failures both times. detekt ktlintCheck clean - it caught two TooGenericExceptionCaught and a long line in this round's own code first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address project announcement review feedback * fix(plugin-store): restrict plugin_downloads RLS to the server-side pipeline (#488) * Batch validation: promote latest reviewed dev changes to main (#463) * Fix #30: Resolve duplicate context menu label collisions * Cleanup: Remove dead code for #91 and #93 * chore: Sanitize SubmitResult.Error at construction rather than at one render site * Fix panel component lifecycle disposal * fixes * Guard panel lifecycle destruction failures * fix(logging): sanitize secret RPC failures and crash-report errors Three related gaps in what gets logged and shown when something fails: - SecretService.kt had no BossLogger in any of its ten catch blocks (#145). When the organisation migration broke all four secret RPCs (#144), the only WARN in the log came from the calling plugin - the code that actually failed said nothing. Every catch now logs via logger.warn(LogCategory.NETWORK, ...), reusing the already-sanitized failure for both the log and the returned Result rather than sanitizing twice. RoleService and RoleCreationService were checked and already log correctly - SecretService was the only silent one. - LogSanitizer.sanitizeExceptionMessage redacted a hostname only when it appeared inside a URL - a bare hostname (exactly what UnknownHostException.getMessage() produces, i.e. every DNS failure, and what a proxy-connect failure looks like) passed through untouched (#109). Added a narrow hostname pattern: lowercase-only labels ending in a short explicit TLD/`.internal`/`.local` list, with a negative lookahead so a package path that happens to end in a real TLD word mid-FQN (kotlinx.coroutines.internal.ScopeCoroutine, kotlinx.io.EOFException) is not mistaken for one - caught by the existing realistic-stack-trace test after the first version of the pattern redacted a live Kotlin package name out of it. - CrashReportService.SubmitResult.Error held a raw exception string, sanitized at exactly one render call site in CrashReportDialog (#110). The constructor is now private; SubmitResult.Error.of(...) is the only way to build one, and it sanitizes before the raw string can reach .message - a property of the type now, not something every future consumer (a copy button, a toast, a log line) has to remember on its own. Both construction sites (the service's two catch blocks, and the one in CrashReportDialog the issue names directly) now route through it. Tests: new coverage for the ten now-logging SecretService paths (via SupabaseWiringTest's existing sanitization-wiring guard, updated to recognize the "sanitize once into a local, reuse for log and return" shape SupabaseDataProviderImpl already used), six new LogSanitizerTest cases for the hostname fix (including the FQN/version-number false-positive guards), and a new CrashReportServiceTest for the sanitize-at-construction guarantee. * test(crash): extract construction tests from #404 Selected unchanged test file from Antriksh1984 original commit 99d2a5228023c541e825fe2714619c8228140369 (PR #404). Maintainer extraction only; subsequent API/scope adaptations are recorded separately. * chore(ipc): clarify advisory menu ID scope and fix formatting * chore(ui): finish obsolete toolbar comment cleanup * test(crash): adapt consolidated coverage and guard factory visibility Maintainer consolidation: use #311 companion invoke and a URL fixture instead of depending on #404 hostname redaction. Add private constructor/copy bytecode assertions. Preserve original contributor tests and implementation credit separately. * refactor(crash): consolidate construction sanitization in #311 Maintainer scope change: remove only the duplicate #110 implementation, retaining Antriksh1984 original #109/#145 work and commit history. Standalone construction tests are retained with attribution in #311; its Aditya8369 implementation supersedes Error.of. Keep render-time sanitization here until #311 merges and describe the remaining hostname coverage conservatively. * 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> * 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> * 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 r…
* Improve toolbox onboarding wizard * Restore terminal setup entry points and background progress * Address onboarding quality checks after main integration * Host persistent terminal setup and route debugging to released Fluck * fix(onboarding): address Fluck handoff review and dialog recovery * fix(onboarding): initialize setup bridge on cold start * fix(onboarding): retain failed tool details before terminal setup --------- Co-authored-by: Shivang <shivang.iitk@gmail.com>
…logging (risa-labs-inc#640) * fix(logging): redact credentials embedded in a URL's userinfo before logging maskUriParams masked only sensitive query and fragment parameters, so a credential carried in a URL's authority (scheme://user:password@host) was returned verbatim and written to the log. A private HTTPS git remote is https://x-access-token:<token>@github.com/..., and DesktopGitService logs the clone URL at INFO and elsewhere branches on repositoryUrl.contains("@") for its auth-failure message, so the URL it logs is expected to carry a token. Browser navigation and deep-link sites pass URLs through the same masker. redactUserInfo now replaces the userinfo with [REDACTED], keeping the scheme, host, port and path. Only an @ inside the authority is treated as a delimiter - the authority ends at the first / ? or # after :// - so an @ in a path or a query value (an email) is left alone, and a URL with no :// is unchanged. The query index is now read off the redacted string, since the redaction can change its length. * fix(logging): redact userinfo in git clone output and nested URLs Review of the first commit found two more places a URL credential reaches the log. cloneRepository merges git's stderr into stdout and logged each line raw at DEBUG. git 2.22 and later strip the credential from their own fatal messages (measured on git 2.43 for 404, 401, 403, 500, redirect and connection failures), but three sources still print it: git older than 2.22, whose release notes say remote-http "did not anonymize URLs reported in its error messages at places"; GIT_TRACE=1 inherited from the environment, which printed the full clone URL four times on git 2.43; and text a server sends back, which git prints verbatim after "remote error:". These lines reach the log only when DEBUG is enabled. The line now goes through cloneProgressLogMessage, which calls the new LogSanitizer.redactUrlUserInfo. It removes only the userinfo, so the failure text, host and port stay readable; sanitizeLogMessage would replace the whole URL and its host. maskUriParams examined only the first authority, so https://sso.example/login?next=https://u:p@internal/ kept its credential. Every :// is now examined. A nested URL's authority also ends at &, so a later &contact=a@b.com is not read as its userinfo. The no-path and repeated-@ cases were already correct and are now pinned, along with an @ as the last authority character. Input without ://, percent-encoded nested URLs and backslash normalization are documented as not handled.
…ive paths to the walk (risa-labs-inc#644) * Respect horizontal scrollers and commit trackpad navigation on release * Fix swipe cancellation, observer lifecycle and terminal delivery * Publish prior contact termination for home swipe attribution * fix(kernel): require verified caller identity for the run-configuration bridge (#634) * fix(overlays): honor RTL and fractional placement (#477) * 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> * 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(db): close signed-in crypto access and verify visibility boundaries * test(db): exercise repeated key rotation and exact anonymous ACLs * test(db): grant fixture role membership on Supabase Postgres * test(db): inline rotation source for isolated pgTAP mounts * test(db): keep generated rotation SQL out of source control * fix(db): reserve identity-taking store mutators for the edge service * fix(db): serialize rotations without requiring direct Vault writes * fix(db): address review gaps in system-org and rotation coverage * test(db): verify core secrets across hex and broker-free rotations * test(db): execute standing audit and missing-signature regression * fix(db): enforce revoke postconditions and harden rotation verification * test(db): enable recovery metadata in the authenticated fixture * test(db): use the schema-supported authenticator type * Address remaining database review quality findings * Fix invoker view audit boolean parsing and test timestamp bounds * Rotate the TOTP envelope, exempt extension routines, and close the repo/prod drift Addresses the handoff's database-integration item and the substantiated findings from the current-head review. Everything below was executed against the PR's own Supabase preview branch, not asserted from reading. **The blocker: rotation refused to run once #417 landed.** The guard was a blanket "TOTP is installed, refuse everything". It is now an adapter. The stored form is 'v1:' || encrypt_text(...) - same cipher, same key, only the framing differs - so the column map gained an envelope prefix and rotates like the rest once the prefix is stripped and re-applied. Three things this needed that reading the diff would not have shown: * #417's trigger RAISES on a v1: value ('TOTP input must be plaintext, not a storage envelope'), so the re-encryption update is rejected outright. The reviewed trigger is now disabled for the update and restored after, inside the same transaction. * safe_decrypt_recovery_codes returns jsonb, not text, so verification through the real read path needed a cast. Both fingerprints now go through each column's OWN application read path rather than decrypt_text, which also removes the step-1/step-5 asymmetry raised as U5. * a step 0 pre-check reports rows that are already unreadable, instead of letting a safe_decrypt_* NULL surface at the end as "a row was missed". The rejection contract is preserved, not removed: an unknown envelope version, an unmapped safe_decrypt_* wrapper, or an unreviewed BEFORE trigger on a mapped table each still refuse. All three are now regression-tested, along with the recovery-code column that was previously never exercised (its fixture was vacuous - create_secret writes no secret_metadata row without p_twofa_enabled). Verified: three consecutive rotations, TOTP/recovery/password all intact, v1 envelope preserved, trigger re-enabled and still enforcing its own contract. **CREATE EXTENSION was impossible.** The fail-closed guard aborted the first routine of any extension installed into public: `create extension pgtap` failed with "Anonymous EXECUTE remains on public.pg_version()". A guard that forces operators to disable it in order to install an extension is a guard that ends up disabled. Extension-owned routines are now exempted with a warning, and the exposure stays visible as advisory CHECK 1x rather than vanishing - it reports pgtap (1079) on the preview. The sweep skips them too: pgcrypto-style helpers are called from column DEFAULTs and CHECK constraints, which are evaluated with the DML role's privileges. No extension owns a function in public on a clean database, so no deployed behaviour changes. **The repo was weaker than production.** find_user_by_email, get_session_status and update_api_key_last_used were locked down on the live project on 2026-09-08 but never captured as a migration, so a fresh deploy re-opened them. find_user_by_email is a user-enumeration oracle over auth.users and signup is open. The standing audit caught this itself on the preview - CHECK 2 named it - which is the drift that check exists for. Both remaining callers use SUPABASE_SERVICE_ROLE_KEY, verified on this branch, so service_role keeps EXECUTE and neither path changes. Also: the guard no longer hard-fails on a database without the Supabase roles (a plain restore), and the vault fixtures no longer assume the key is absent (U3) - both suites failed outright on any database that already had one. Retained-key assertions measure this run's delta instead of an absolute count. Suites on the preview: rotation 16/16, db access audit, and visibility 54/54 all clean; the standing audit reports no failing gated check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Exercise the anonymous read path, not just the grant The keep-list assertions only checked has_function_privilege for the seven allowlisted signatures. That proves the grant, not the path: an RLS policy expression is evaluated as the QUERYING role, so revoking a policy helper turns an anonymous SELECT into 'permission denied for function ...' rather than an empty result, and no privilege assertion can see it. The seeding is the part that matters. A policy expression runs PER ROW, so on an empty table it is never evaluated - and plugins and user_roles are both empty on a fresh database, which made the first version of this probe pass no matter what had been revoked. Caught by trying to make it fail. Sensitivity is stated from measurement, not assumption: with a row present, revoking can_view_plugin_row from anon does fail the probe and revoking authorize does not, because permissive policies are ORed and short-circuit. A helper in a policy that never has to be evaluated cannot be detected this way by any test, so the comment says so rather than implying broader coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the gated audit checks and the test generator provable Both halves of this are the same failure: a suite that is green while proving nothing. I hit that twice writing the previous commits - a recovery-code fixture over a secret_metadata row create_secret never wrote, and an anonymous-read probe over an empty table whose RLS policy therefore never evaluated - so these are not hypothetical. Finding 6. The four CI-GATED checks (1, 1b, 4, 5) only ever asserted HEALTHY, while every advisory check already had a fixture-based detection test. An inverted `not exists`, a wrong role literal or a typo in the evtenabled test would have left the gate permanently green. Each is now broken deliberately and asserted to report it: a routine granted to anon AFTER creation (which is how one gets past the event trigger), a revoked deliberate anonymous grant, a disabled event trigger, and a crypto routine exposed to authenticated. They have teeth by construction - the assertion is that the finding appears, so a broken audit query fails them. Finding 8. The generator gained --check, which regenerates in memory and compares without writing, and orphan removal for a generated suite whose .sql.in has been renamed or deleted - that file otherwise keeps being executed forever with stale content. Orphans are identified by a provenance banner rather than a hard-coded list, so a hand-written suite is never touched; verified that explicit_anon_and_org_visibility_test.sql is untouched. Each guard was proven by making it fire: a hand edit and a planted orphan are both reported by --check, and generate removes the orphan. CI now runs generate and --check as their own step, so a preparation failure is visible in the log instead of being attributed to the test run. Preview: rotation 16/16, db access audit 15/15, visibility 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Announce every project change, not only the plugin-initiated ones Panels could come up empty and stay empty, and the reason was that the startup restore told no one. ProjectChangeEvent was published from ProjectDataProviderImpl.selectProject, which is the path a PLUGIN takes. The two callers that matter most bypass it: WorkspaceApplier.applyWorkspace calls windowProjectState.selectProject directly when it restores the workspace, and BossTopBar's picker goes through selectProjectInWindow. Neither published anything. The ordering that produced the empty panel: DefaultPlugin is constructed in a DisposableEffect keyed on registries and window state, not on selectedProject, and it kicks off the plugin scan asynchronously while BossAppScaffold composes and the sidebar builds panels immediately. The restore meanwhile waits on WorkspaceManager's sequential Dispatchers.IO JSON reads. When those reads are slow the panel is built first, reads the "" that WindowProjectState seeds itself with, and renders its no-project state. The restore then lands, silently, and nothing remounts the cached component. Publishing from the state itself rather than from one caller catches all three. The bus is MutableSharedFlow(replay = 0), so a publish that never happens cannot be recovered by a later subscriber - which is why this could not stay per-caller. The publish is removed from selectProject rather than added alongside the collector; keeping both would double-fire on the plugin path. previousPath is seeded from the current value so the StateFlow's replay of it is not announced as a change. That seed is "" at startup, and telling every plugin the project just became "" moments before the real restore arrives is precisely the clear-yourself signal worth avoiding. One collector per window: projectDataProvider is `by lazy` on DefaultPlugin, which is per-window, so this cannot stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Publish from the state's own callback, not from a lazily-built provider The review's finding 1: observing `selectedProject` from `ProjectDataProviderImpl` put the fix behind two `by lazy` initializers. `projectDataProvider` and `applicationEventBus` are both lazy on `DefaultPlugin`, and the second is the only host path that creates the bus at all. So the collector started only if some plugin touched both before the workspace JSON reads finished - and "the reads are slow" is the premise of the bug. Touched after the restore, the collector seeds to the restored path and announces nothing; never touched, no caller is announced at all. The announcement now hangs off the `ProjectSelectionCallback` that `WindowProjectStateRegistry` installs when it builds the window's state, which `BossAppState` does eagerly in composition. `WindowProjectState.selectProject` is the sole mutator of the selection and invokes that callback synchronously, so every caller is covered with no coroutine, no scope and nothing lazy in the path. Being synchronous also makes `previousProjectPath` a faithful history rather than best-effort: there is no StateFlow conflation to collapse A -> B -> C into A -> C. The two registry entry points carried a copy of the wiring each; they now share one `newState`, so the announcement cannot be installed on some windows and not others. Finding 1's third mitigation, which is a hole of its own: `publishSystemEvent` was a no-op whenever the bus had not been created, so on a build where no installed plugin had touched `applicationEventBus` yet, no host event existed - not `ProjectChangeEvent`, not `AuthEvent`, not `TabEvent` - and with `replay = 0` none could be recovered. It now creates the bus instead of dropping. `getInstance` also re-checks the registry on every call rather than only at creation, so an instance that exists while the registry is empty can no longer strand the host's publisher. Findings 2 and 3: `ProjectDataProviderImpl` implements `DisposableProvider` and `DefaultPlugin.dispose()` releases it through a named delegate, alongside `logDataProviderDelegate` and `gitDataProviderDelegate`; its scope gets a `SupervisorJob`, matching `pluginScope`. Tests: `ProjectChangeAnnouncementTest` (6) pins a direct `selectProject` being announced, the previous-path chain, the seeding rule, the same-path suppression, both registry entry points, and the bus being created rather than dropping the event. Full `:composeApp:desktopTest` green: 3695 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 2: honest bus wording, atomic previousPath, three more tests Review (1/2) and (2/2). 1. `publishSystemEvent` does not save the event it creates the bus for. Correct - no registered publisher means nobody holds the bus, so it has no subscribers and that first event still reaches no one. The comment, the test name and the PR body all claimed otherwise. Reworded to what it actually buys: the host stops being permanently silent while it waits for a plugin to touch `applicationEventBus`. 3. `previousPath` was check-then-act on a plain field, and `ProjectDataServiceBridge.selectProject` is a suspend gRPC handler with no hop to Main (verified: `KernelBootstrap` registers it, the bridge calls `provider.selectProject` directly). Now an `AtomicReference.getAndSet`, so the read and the write are one operation. 5. The registry read-modify-write moved back inside `synchronized(this)`. Both fields are in fact `@Volatile` in boss-plugin-api 1.0.87 (checked the pinned jar), but the lock is uncontended after the first call and removes the question. The bus-before-publisher ordering is now written down, including why a publish landing in that window loses nothing. 6. `initialPath` KDoc no longer describes a production scenario it cannot reach; it is documented as defensive and test-reachable, and defaulted to "". 7. Three tests added: - a plugin-initiated selection is announced exactly once (catches both a silent regression to zero and an accidentally re-added publish); - the registry test now asserts the recent-projects half of the callback ran - deleting it from `newState` previously left every test green; - `dispose()` stops the recent-projects collector, which is the whole reason `projectDataProviderDelegate` became a named lazy. `Dispatchers.setMain(UnconfinedTestDispatcher())` for the class: the provider collects on `Dispatchers.Main`, which has no implementation in a plain test JVM, so its collector silently never ran. Unconfined also makes the dispose test deterministic rather than a sleep. Smaller: `SupervisorJob` comment describes it as future-proofing (there is one collector); `systemEventBusScope` is `by lazy`; the KERNEL-mode consequence of disposing the provider is documented on `dispose()`. Not taken: `replay = 1` for `ProjectChangeEvent`. It would close the mirror race, but it changes `events()` semantics for every subscriber and every event type on a bus whose consumers are out of tree. The body now says the race is narrowed rather than closed, and names the api-side `selectedProject` StateFlow as what closes it. `:composeApp:desktopTest` 3697 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 3: unconditional announcement, ordered publish, drop dead register Review 3. 2. AGENTS.md: a bullet in the canonical "what a third-party plugin can observe" paragraph, since this widens *when* a filesystem path reaches every installed plugin. 3. The `expect` KDoc still said "a no-op if the bus has not been created yet". Only the `actual` had been corrected. Fixed, and it now states the part that IS still best-effort: replay = 0 means an unsubscribed event is gone either way. 4. The announcement sat downstream of `ProjectState.updateRecentProjects`, so a throw there skipped it AND left `previousPath` stale - every later selection reporting a previous path one step behind, silently, for the session. Now `try { recents } finally { announce }`: order preserved for anyone reading recents off the event, the announcement unconditional, the throw still propagating. 5. `getAndSet` closed the read-modify-write but not the publish order, so two concurrent selections could emit out of order. `previousPath` and the publish are now in one `synchronized` block (the AtomicReference is gone - the lock subsumes it). The KDoc says plainly what this still does not settle: `WindowProjectState` writes `_selectedProject.value` then calls the callback with no atomicity, so state and last event can still disagree. Closing that means locking upstream. 6. `ProjectState`'s async `loadRecentProjects()` assigns `_recentProjects.value` wholesale and could land mid-test, dropping a just-added path - only where `~/.boss/recent-projects.json` exists, so a developer-machine-only flake. A once-per-JVM settle in `@BeforeTest` puts that single file read before any test. The test paths stay deliberately non-existent, now with a comment saying why: the fire-and-forget saves are unordered, and `loadRecentProjects` reclaims entries whose directory is gone, so a real temp directory would leak where these self-heal. 7. `register` deleted. No production caller (`BossAppState:299` uses `getOrCreate`), and it overwrote an existing entry - handing back a state with a fresh announcer seeded to "" for a window that already had a project. `newState` stays; it is what keeps the wiring from drifting. 8. `@Volatile` dropped from `ApplicationEventBusImpl.instance` - every access is inside the lock now, and leaving it reads as if a fast path survives. Also: a test for the actual regression path, `applyWorkspace` announcing the project it restores, rather than a comment claiming to imitate it. Answering the grep you could not run: the only `projectChanges()` consumers in boss_plugins are the two fluck-agent panels (`FluckAgentViewModel`), and both assign `_bossProject.value` and re-sweep - idempotent on a repeat. Nothing used the repeat publish as a reload nudge. Recorded in the announcer comment. Item 1 was already in the PR body from round 2 ("The startup race is narrowed, not closed"), naming the mirror case and the api-side StateFlow that closes it. `:composeApp:desktopTest` 3698 tests / 368 classes, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 4: split the dispose out, stop tests writing the real ~/.boss Review 4. Items 1 and 3 were the before-merge ones. 1. Verified: ProjectDataServiceBridge.watchRecentProjects collects provider.recentProjects, a StateFlow, so cancelling the provider's scope on window close leaves that gRPC stream open and silently frozen for every out-of-process plugin. Taking the first option and SPLITTING the dispose out. Note this reverses what round 1 asked for, and the justification changed rather than the opinion: once the announcement moved to the registry callback, ProjectDataProviderImpl went back to being exactly what it was before this PR, so the leak is pre-existing and unrelated to the subject. Closing it properly means the bridge reading ProjectState directly instead of a per-window provider, which is its own change. The reasoning is on the class so it is not re-fixed by accident. 3. Verified and worse than described: MAX_RECENT_PROJECTS = 10, so test entries evict real ones from the developer's picker and no cleanup restores them. systemProperty("user.home", <build>/test-home) on the Test task. One run with it in place shows the existing suite was writing far more than recents to the real home - window-appearance-settings.json, keymap-settings.json, scrollbar-settings.json, recent-browser-pages.json, dashboard-stats.json and a ~/BossProjects/ directory all land in test-home now. The settle is gone with it: no recent-projects.json in test-home means the wholesale reload never happens. 2. publishSystemEvent now refuses the bus != null && systemPublisher == null state instead of falling through into a bus that may not be the registry's, with one warning rather than one per event. The comment at the fallback is true as written. 4. Both stale comments fixed (one construction path, not two). 5. hostProjectCallback(updateRecents, announcer) extracted so the try/finally claim is testable - ProjectState is an object, so a hard-coded call cannot be made to fail. 6. The publish-inside-the-lock trade is spelled out in the KDoc: what it buys (ordering), what it costs (an inline subscriber runs holding the lock inside selectProject), and that the contended path is untested. 7. The serial-execution dependency is recorded in the test class KDoc. Plus: multi-window isolation, and a test that makes the recents update throw. One thing this round caused and fixed: removing dispose() left two ProjectDataProviderImpl collectors alive on the test dispatcher. On a TestCoroutineScheduler a live coroutine is something every later runTest waits on, and the git classes timed out at 60s in full-suite runs while passing in isolation. Dispatchers.setMain(Dispatchers.Unconfined) rather than UnconfinedTestDispatcher(): same inline behaviour, not enrolled in a scheduler anyone else observes. It is also a real argument for the follow-up - without dispose() the provider cannot be constructed in a test without leaking. :composeApp:desktopTest 3699 tests / 368 classes, 0 failures. detekt ktlintCheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 5: reset test-home per run, stop getInstance stealing a publisher Review 5. 1. Confirmed reproducibly - build/test-home/.boss/recent-projects.json was sitting there from the previous run. doFirst now deleteRecursively() before mkdirs(), so the home is fresh per run rather than merely private. Without it the redirect only helped the first run and ProjectState.init's load reintroduced the same race from the second. Verified by running the full suite twice back to back. 2. Right, and sharper than it looks: the guard tested `bus` but wrote both fields, and `systemPublisher != null && bus == null` is exactly what ProjectChangeAnnouncementTest, BrowserAnalyticsEmissionTest and BossTabsComponentMoveTest install. Guard now tests both, so getInstance cannot take a publisher away from whoever set it. 3. Test added for the warn-and-drop branch, asserting the DROP rather than the warning - partialRegistryWarned is a one-shot process global, so "it warns" is unassertable after the first test to trip it. Uses a stub bus so the half-registry state is real. 4. The re-entrancy hazard is now documented as SAFE and why (synchronized is reentrant, previousPath advances before the publish), so it is not "fixed" later. The lock-widens-a-UI-hang-to-a-cross-thread-one point is in the same block. 5. Taken, including the optional ones: - hostProjectCallback keeps both failures (addSuppressed) instead of letting finally discard the first. Needs @Suppress("TooGenericExceptionCaught") - catching Throwable is the contract, and both are rethrown. - ProjectDataProviderImpl takes an injectable dispatcher (default Dispatchers.Main). This removes the global setMain AND the round-4 collector leak at the source rather than working around them, without reopening the DisposableProvider split. - Three more pins: a null window state announces nothing, applyWorkspace (restoreProject = false) announces nothing, unregister + getOrCreate restarts the chain at "". Not taken, with reasons: jvmArgumentProviders (the current form works, is config-cache-serialisable and passed CI on three OSes); widening the user.home redirect to plugin-path-utils (its test only mkdirs a directory every BOSS user already has). There is no CHANGELOG in this repo, so the release-notes line for the same-path change is an action item at release time, not a file change. Flagged in the body. Two consecutive full runs: 3703 tests / 368 classes, 0 failures both times. detekt ktlintCheck clean - it caught two TooGenericExceptionCaught and a long line in this round's own code first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address project announcement review feedback * fix(plugin-store): restrict plugin_downloads RLS to the server-side pipeline (#488) * Batch validation: promote latest reviewed dev changes to main (#463) * Fix #30: Resolve duplicate context menu label collisions * Cleanup: Remove dead code for #91 and #93 * chore: Sanitize SubmitResult.Error at construction rather than at one render site * Fix panel component lifecycle disposal * fixes * Guard panel lifecycle destruction failures * fix(logging): sanitize secret RPC failures and crash-report errors Three related gaps in what gets logged and shown when something fails: - SecretService.kt had no BossLogger in any of its ten catch blocks (#145). When the organisation migration broke all four secret RPCs (#144), the only WARN in the log came from the calling plugin - the code that actually failed said nothing. Every catch now logs via logger.warn(LogCategory.NETWORK, ...), reusing the already-sanitized failure for both the log and the returned Result rather than sanitizing twice. RoleService and RoleCreationService were checked and already log correctly - SecretService was the only silent one. - LogSanitizer.sanitizeExceptionMessage redacted a hostname only when it appeared inside a URL - a bare hostname (exactly what UnknownHostException.getMessage() produces, i.e. every DNS failure, and what a proxy-connect failure looks like) passed through untouched (#109). Added a narrow hostname pattern: lowercase-only labels ending in a short explicit TLD/`.internal`/`.local` list, with a negative lookahead so a package path that happens to end in a real TLD word mid-FQN (kotlinx.coroutines.internal.ScopeCoroutine, kotlinx.io.EOFException) is not mistaken for one - caught by the existing realistic-stack-trace test after the first version of the pattern redacted a live Kotlin package name out of it. - CrashReportService.SubmitResult.Error held a raw exception string, sanitized at exactly one render call site in CrashReportDialog (#110). The constructor is now private; SubmitResult.Error.of(...) is the only way to build one, and it sanitizes before the raw string can reach .message - a property of the type now, not something every future consumer (a copy button, a toast, a log line) has to remember on its own. Both construction sites (the service's two catch blocks, and the one in CrashReportDialog the issue names directly) now route through it. Tests: new coverage for the ten now-logging SecretService paths (via SupabaseWiringTest's existing sanitization-wiring guard, updated to recognize the "sanitize once into a local, reuse for log and return" shape SupabaseDataProviderImpl already used), six new LogSanitizerTest cases for the hostname fix (including the FQN/version-number false-positive guards), and a new CrashReportServiceTest for the sanitize-at-construction guarantee. * test(crash): extract construction tests from #404 Selected unchanged test file from Antriksh1984 original commit 99d2a5228023c541e825fe2714619c8228140369 (PR #404). Maintainer extraction only; subsequent API/scope adaptations are recorded separately. * chore(ipc): clarify advisory menu ID scope and fix formatting * chore(ui): finish obsolete toolbar comment cleanup * test(crash): adapt consolidated coverage and guard factory visibility Maintainer consolidation: use #311 companion invoke and a URL fixture instead of depending on #404 hostname redaction. Add private constructor/copy bytecode assertions. Preserve original contributor tests and implementation credit separately. * refactor(crash): consolidate construction sanitization in #311 Maintainer scope change: remove only the duplicate #110 implementation, retaining Antriksh1984 original #109/#145 work and commit history. Standalone construction tests are retained with attribution in #311; its Aditya8369 implementation supersedes Error.of. Keep render-time sanitization here until #311 merges and describe the remaining hostname coverage conservatively. * 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> * 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> * 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. * Serializ…
…SIX (risa-labs-inc#705) * Respect horizontal scrollers and commit trackpad navigation on release * Fix swipe cancellation, observer lifecycle and terminal delivery * Publish prior contact termination for home swipe attribution * fix(kernel): require verified caller identity for the run-configuration bridge (#634) * fix(overlays): honor RTL and fractional placement (#477) * 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> * 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(db): close signed-in crypto access and verify visibility boundaries * test(db): exercise repeated key rotation and exact anonymous ACLs * test(db): grant fixture role membership on Supabase Postgres * test(db): inline rotation source for isolated pgTAP mounts * test(db): keep generated rotation SQL out of source control * fix(db): reserve identity-taking store mutators for the edge service * fix(db): serialize rotations without requiring direct Vault writes * fix(db): address review gaps in system-org and rotation coverage * test(db): verify core secrets across hex and broker-free rotations * test(db): execute standing audit and missing-signature regression * fix(db): enforce revoke postconditions and harden rotation verification * test(db): enable recovery metadata in the authenticated fixture * test(db): use the schema-supported authenticator type * Address remaining database review quality findings * Fix invoker view audit boolean parsing and test timestamp bounds * Rotate the TOTP envelope, exempt extension routines, and close the repo/prod drift Addresses the handoff's database-integration item and the substantiated findings from the current-head review. Everything below was executed against the PR's own Supabase preview branch, not asserted from reading. **The blocker: rotation refused to run once #417 landed.** The guard was a blanket "TOTP is installed, refuse everything". It is now an adapter. The stored form is 'v1:' || encrypt_text(...) - same cipher, same key, only the framing differs - so the column map gained an envelope prefix and rotates like the rest once the prefix is stripped and re-applied. Three things this needed that reading the diff would not have shown: * #417's trigger RAISES on a v1: value ('TOTP input must be plaintext, not a storage envelope'), so the re-encryption update is rejected outright. The reviewed trigger is now disabled for the update and restored after, inside the same transaction. * safe_decrypt_recovery_codes returns jsonb, not text, so verification through the real read path needed a cast. Both fingerprints now go through each column's OWN application read path rather than decrypt_text, which also removes the step-1/step-5 asymmetry raised as U5. * a step 0 pre-check reports rows that are already unreadable, instead of letting a safe_decrypt_* NULL surface at the end as "a row was missed". The rejection contract is preserved, not removed: an unknown envelope version, an unmapped safe_decrypt_* wrapper, or an unreviewed BEFORE trigger on a mapped table each still refuse. All three are now regression-tested, along with the recovery-code column that was previously never exercised (its fixture was vacuous - create_secret writes no secret_metadata row without p_twofa_enabled). Verified: three consecutive rotations, TOTP/recovery/password all intact, v1 envelope preserved, trigger re-enabled and still enforcing its own contract. **CREATE EXTENSION was impossible.** The fail-closed guard aborted the first routine of any extension installed into public: `create extension pgtap` failed with "Anonymous EXECUTE remains on public.pg_version()". A guard that forces operators to disable it in order to install an extension is a guard that ends up disabled. Extension-owned routines are now exempted with a warning, and the exposure stays visible as advisory CHECK 1x rather than vanishing - it reports pgtap (1079) on the preview. The sweep skips them too: pgcrypto-style helpers are called from column DEFAULTs and CHECK constraints, which are evaluated with the DML role's privileges. No extension owns a function in public on a clean database, so no deployed behaviour changes. **The repo was weaker than production.** find_user_by_email, get_session_status and update_api_key_last_used were locked down on the live project on 2026-09-08 but never captured as a migration, so a fresh deploy re-opened them. find_user_by_email is a user-enumeration oracle over auth.users and signup is open. The standing audit caught this itself on the preview - CHECK 2 named it - which is the drift that check exists for. Both remaining callers use SUPABASE_SERVICE_ROLE_KEY, verified on this branch, so service_role keeps EXECUTE and neither path changes. Also: the guard no longer hard-fails on a database without the Supabase roles (a plain restore), and the vault fixtures no longer assume the key is absent (U3) - both suites failed outright on any database that already had one. Retained-key assertions measure this run's delta instead of an absolute count. Suites on the preview: rotation 16/16, db access audit, and visibility 54/54 all clean; the standing audit reports no failing gated check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Exercise the anonymous read path, not just the grant The keep-list assertions only checked has_function_privilege for the seven allowlisted signatures. That proves the grant, not the path: an RLS policy expression is evaluated as the QUERYING role, so revoking a policy helper turns an anonymous SELECT into 'permission denied for function ...' rather than an empty result, and no privilege assertion can see it. The seeding is the part that matters. A policy expression runs PER ROW, so on an empty table it is never evaluated - and plugins and user_roles are both empty on a fresh database, which made the first version of this probe pass no matter what had been revoked. Caught by trying to make it fail. Sensitivity is stated from measurement, not assumption: with a row present, revoking can_view_plugin_row from anon does fail the probe and revoking authorize does not, because permissive policies are ORed and short-circuit. A helper in a policy that never has to be evaluated cannot be detected this way by any test, so the comment says so rather than implying broader coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the gated audit checks and the test generator provable Both halves of this are the same failure: a suite that is green while proving nothing. I hit that twice writing the previous commits - a recovery-code fixture over a secret_metadata row create_secret never wrote, and an anonymous-read probe over an empty table whose RLS policy therefore never evaluated - so these are not hypothetical. Finding 6. The four CI-GATED checks (1, 1b, 4, 5) only ever asserted HEALTHY, while every advisory check already had a fixture-based detection test. An inverted `not exists`, a wrong role literal or a typo in the evtenabled test would have left the gate permanently green. Each is now broken deliberately and asserted to report it: a routine granted to anon AFTER creation (which is how one gets past the event trigger), a revoked deliberate anonymous grant, a disabled event trigger, and a crypto routine exposed to authenticated. They have teeth by construction - the assertion is that the finding appears, so a broken audit query fails them. Finding 8. The generator gained --check, which regenerates in memory and compares without writing, and orphan removal for a generated suite whose .sql.in has been renamed or deleted - that file otherwise keeps being executed forever with stale content. Orphans are identified by a provenance banner rather than a hard-coded list, so a hand-written suite is never touched; verified that explicit_anon_and_org_visibility_test.sql is untouched. Each guard was proven by making it fire: a hand edit and a planted orphan are both reported by --check, and generate removes the orphan. CI now runs generate and --check as their own step, so a preparation failure is visible in the log instead of being attributed to the test run. Preview: rotation 16/16, db access audit 15/15, visibility 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Announce every project change, not only the plugin-initiated ones Panels could come up empty and stay empty, and the reason was that the startup restore told no one. ProjectChangeEvent was published from ProjectDataProviderImpl.selectProject, which is the path a PLUGIN takes. The two callers that matter most bypass it: WorkspaceApplier.applyWorkspace calls windowProjectState.selectProject directly when it restores the workspace, and BossTopBar's picker goes through selectProjectInWindow. Neither published anything. The ordering that produced the empty panel: DefaultPlugin is constructed in a DisposableEffect keyed on registries and window state, not on selectedProject, and it kicks off the plugin scan asynchronously while BossAppScaffold composes and the sidebar builds panels immediately. The restore meanwhile waits on WorkspaceManager's sequential Dispatchers.IO JSON reads. When those reads are slow the panel is built first, reads the "" that WindowProjectState seeds itself with, and renders its no-project state. The restore then lands, silently, and nothing remounts the cached component. Publishing from the state itself rather than from one caller catches all three. The bus is MutableSharedFlow(replay = 0), so a publish that never happens cannot be recovered by a later subscriber - which is why this could not stay per-caller. The publish is removed from selectProject rather than added alongside the collector; keeping both would double-fire on the plugin path. previousPath is seeded from the current value so the StateFlow's replay of it is not announced as a change. That seed is "" at startup, and telling every plugin the project just became "" moments before the real restore arrives is precisely the clear-yourself signal worth avoiding. One collector per window: projectDataProvider is `by lazy` on DefaultPlugin, which is per-window, so this cannot stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Publish from the state's own callback, not from a lazily-built provider The review's finding 1: observing `selectedProject` from `ProjectDataProviderImpl` put the fix behind two `by lazy` initializers. `projectDataProvider` and `applicationEventBus` are both lazy on `DefaultPlugin`, and the second is the only host path that creates the bus at all. So the collector started only if some plugin touched both before the workspace JSON reads finished - and "the reads are slow" is the premise of the bug. Touched after the restore, the collector seeds to the restored path and announces nothing; never touched, no caller is announced at all. The announcement now hangs off the `ProjectSelectionCallback` that `WindowProjectStateRegistry` installs when it builds the window's state, which `BossAppState` does eagerly in composition. `WindowProjectState.selectProject` is the sole mutator of the selection and invokes that callback synchronously, so every caller is covered with no coroutine, no scope and nothing lazy in the path. Being synchronous also makes `previousProjectPath` a faithful history rather than best-effort: there is no StateFlow conflation to collapse A -> B -> C into A -> C. The two registry entry points carried a copy of the wiring each; they now share one `newState`, so the announcement cannot be installed on some windows and not others. Finding 1's third mitigation, which is a hole of its own: `publishSystemEvent` was a no-op whenever the bus had not been created, so on a build where no installed plugin had touched `applicationEventBus` yet, no host event existed - not `ProjectChangeEvent`, not `AuthEvent`, not `TabEvent` - and with `replay = 0` none could be recovered. It now creates the bus instead of dropping. `getInstance` also re-checks the registry on every call rather than only at creation, so an instance that exists while the registry is empty can no longer strand the host's publisher. Findings 2 and 3: `ProjectDataProviderImpl` implements `DisposableProvider` and `DefaultPlugin.dispose()` releases it through a named delegate, alongside `logDataProviderDelegate` and `gitDataProviderDelegate`; its scope gets a `SupervisorJob`, matching `pluginScope`. Tests: `ProjectChangeAnnouncementTest` (6) pins a direct `selectProject` being announced, the previous-path chain, the seeding rule, the same-path suppression, both registry entry points, and the bus being created rather than dropping the event. Full `:composeApp:desktopTest` green: 3695 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 2: honest bus wording, atomic previousPath, three more tests Review (1/2) and (2/2). 1. `publishSystemEvent` does not save the event it creates the bus for. Correct - no registered publisher means nobody holds the bus, so it has no subscribers and that first event still reaches no one. The comment, the test name and the PR body all claimed otherwise. Reworded to what it actually buys: the host stops being permanently silent while it waits for a plugin to touch `applicationEventBus`. 3. `previousPath` was check-then-act on a plain field, and `ProjectDataServiceBridge.selectProject` is a suspend gRPC handler with no hop to Main (verified: `KernelBootstrap` registers it, the bridge calls `provider.selectProject` directly). Now an `AtomicReference.getAndSet`, so the read and the write are one operation. 5. The registry read-modify-write moved back inside `synchronized(this)`. Both fields are in fact `@Volatile` in boss-plugin-api 1.0.87 (checked the pinned jar), but the lock is uncontended after the first call and removes the question. The bus-before-publisher ordering is now written down, including why a publish landing in that window loses nothing. 6. `initialPath` KDoc no longer describes a production scenario it cannot reach; it is documented as defensive and test-reachable, and defaulted to "". 7. Three tests added: - a plugin-initiated selection is announced exactly once (catches both a silent regression to zero and an accidentally re-added publish); - the registry test now asserts the recent-projects half of the callback ran - deleting it from `newState` previously left every test green; - `dispose()` stops the recent-projects collector, which is the whole reason `projectDataProviderDelegate` became a named lazy. `Dispatchers.setMain(UnconfinedTestDispatcher())` for the class: the provider collects on `Dispatchers.Main`, which has no implementation in a plain test JVM, so its collector silently never ran. Unconfined also makes the dispose test deterministic rather than a sleep. Smaller: `SupervisorJob` comment describes it as future-proofing (there is one collector); `systemEventBusScope` is `by lazy`; the KERNEL-mode consequence of disposing the provider is documented on `dispose()`. Not taken: `replay = 1` for `ProjectChangeEvent`. It would close the mirror race, but it changes `events()` semantics for every subscriber and every event type on a bus whose consumers are out of tree. The body now says the race is narrowed rather than closed, and names the api-side `selectedProject` StateFlow as what closes it. `:composeApp:desktopTest` 3697 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 3: unconditional announcement, ordered publish, drop dead register Review 3. 2. AGENTS.md: a bullet in the canonical "what a third-party plugin can observe" paragraph, since this widens *when* a filesystem path reaches every installed plugin. 3. The `expect` KDoc still said "a no-op if the bus has not been created yet". Only the `actual` had been corrected. Fixed, and it now states the part that IS still best-effort: replay = 0 means an unsubscribed event is gone either way. 4. The announcement sat downstream of `ProjectState.updateRecentProjects`, so a throw there skipped it AND left `previousPath` stale - every later selection reporting a previous path one step behind, silently, for the session. Now `try { recents } finally { announce }`: order preserved for anyone reading recents off the event, the announcement unconditional, the throw still propagating. 5. `getAndSet` closed the read-modify-write but not the publish order, so two concurrent selections could emit out of order. `previousPath` and the publish are now in one `synchronized` block (the AtomicReference is gone - the lock subsumes it). The KDoc says plainly what this still does not settle: `WindowProjectState` writes `_selectedProject.value` then calls the callback with no atomicity, so state and last event can still disagree. Closing that means locking upstream. 6. `ProjectState`'s async `loadRecentProjects()` assigns `_recentProjects.value` wholesale and could land mid-test, dropping a just-added path - only where `~/.boss/recent-projects.json` exists, so a developer-machine-only flake. A once-per-JVM settle in `@BeforeTest` puts that single file read before any test. The test paths stay deliberately non-existent, now with a comment saying why: the fire-and-forget saves are unordered, and `loadRecentProjects` reclaims entries whose directory is gone, so a real temp directory would leak where these self-heal. 7. `register` deleted. No production caller (`BossAppState:299` uses `getOrCreate`), and it overwrote an existing entry - handing back a state with a fresh announcer seeded to "" for a window that already had a project. `newState` stays; it is what keeps the wiring from drifting. 8. `@Volatile` dropped from `ApplicationEventBusImpl.instance` - every access is inside the lock now, and leaving it reads as if a fast path survives. Also: a test for the actual regression path, `applyWorkspace` announcing the project it restores, rather than a comment claiming to imitate it. Answering the grep you could not run: the only `projectChanges()` consumers in boss_plugins are the two fluck-agent panels (`FluckAgentViewModel`), and both assign `_bossProject.value` and re-sweep - idempotent on a repeat. Nothing used the repeat publish as a reload nudge. Recorded in the announcer comment. Item 1 was already in the PR body from round 2 ("The startup race is narrowed, not closed"), naming the mirror case and the api-side StateFlow that closes it. `:composeApp:desktopTest` 3698 tests / 368 classes, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 4: split the dispose out, stop tests writing the real ~/.boss Review 4. Items 1 and 3 were the before-merge ones. 1. Verified: ProjectDataServiceBridge.watchRecentProjects collects provider.recentProjects, a StateFlow, so cancelling the provider's scope on window close leaves that gRPC stream open and silently frozen for every out-of-process plugin. Taking the first option and SPLITTING the dispose out. Note this reverses what round 1 asked for, and the justification changed rather than the opinion: once the announcement moved to the registry callback, ProjectDataProviderImpl went back to being exactly what it was before this PR, so the leak is pre-existing and unrelated to the subject. Closing it properly means the bridge reading ProjectState directly instead of a per-window provider, which is its own change. The reasoning is on the class so it is not re-fixed by accident. 3. Verified and worse than described: MAX_RECENT_PROJECTS = 10, so test entries evict real ones from the developer's picker and no cleanup restores them. systemProperty("user.home", <build>/test-home) on the Test task. One run with it in place shows the existing suite was writing far more than recents to the real home - window-appearance-settings.json, keymap-settings.json, scrollbar-settings.json, recent-browser-pages.json, dashboard-stats.json and a ~/BossProjects/ directory all land in test-home now. The settle is gone with it: no recent-projects.json in test-home means the wholesale reload never happens. 2. publishSystemEvent now refuses the bus != null && systemPublisher == null state instead of falling through into a bus that may not be the registry's, with one warning rather than one per event. The comment at the fallback is true as written. 4. Both stale comments fixed (one construction path, not two). 5. hostProjectCallback(updateRecents, announcer) extracted so the try/finally claim is testable - ProjectState is an object, so a hard-coded call cannot be made to fail. 6. The publish-inside-the-lock trade is spelled out in the KDoc: what it buys (ordering), what it costs (an inline subscriber runs holding the lock inside selectProject), and that the contended path is untested. 7. The serial-execution dependency is recorded in the test class KDoc. Plus: multi-window isolation, and a test that makes the recents update throw. One thing this round caused and fixed: removing dispose() left two ProjectDataProviderImpl collectors alive on the test dispatcher. On a TestCoroutineScheduler a live coroutine is something every later runTest waits on, and the git classes timed out at 60s in full-suite runs while passing in isolation. Dispatchers.setMain(Dispatchers.Unconfined) rather than UnconfinedTestDispatcher(): same inline behaviour, not enrolled in a scheduler anyone else observes. It is also a real argument for the follow-up - without dispose() the provider cannot be constructed in a test without leaking. :composeApp:desktopTest 3699 tests / 368 classes, 0 failures. detekt ktlintCheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 5: reset test-home per run, stop getInstance stealing a publisher Review 5. 1. Confirmed reproducibly - build/test-home/.boss/recent-projects.json was sitting there from the previous run. doFirst now deleteRecursively() before mkdirs(), so the home is fresh per run rather than merely private. Without it the redirect only helped the first run and ProjectState.init's load reintroduced the same race from the second. Verified by running the full suite twice back to back. 2. Right, and sharper than it looks: the guard tested `bus` but wrote both fields, and `systemPublisher != null && bus == null` is exactly what ProjectChangeAnnouncementTest, BrowserAnalyticsEmissionTest and BossTabsComponentMoveTest install. Guard now tests both, so getInstance cannot take a publisher away from whoever set it. 3. Test added for the warn-and-drop branch, asserting the DROP rather than the warning - partialRegistryWarned is a one-shot process global, so "it warns" is unassertable after the first test to trip it. Uses a stub bus so the half-registry state is real. 4. The re-entrancy hazard is now documented as SAFE and why (synchronized is reentrant, previousPath advances before the publish), so it is not "fixed" later. The lock-widens-a-UI-hang-to-a-cross-thread-one point is in the same block. 5. Taken, including the optional ones: - hostProjectCallback keeps both failures (addSuppressed) instead of letting finally discard the first. Needs @Suppress("TooGenericExceptionCaught") - catching Throwable is the contract, and both are rethrown. - ProjectDataProviderImpl takes an injectable dispatcher (default Dispatchers.Main). This removes the global setMain AND the round-4 collector leak at the source rather than working around them, without reopening the DisposableProvider split. - Three more pins: a null window state announces nothing, applyWorkspace (restoreProject = false) announces nothing, unregister + getOrCreate restarts the chain at "". Not taken, with reasons: jvmArgumentProviders (the current form works, is config-cache-serialisable and passed CI on three OSes); widening the user.home redirect to plugin-path-utils (its test only mkdirs a directory every BOSS user already has). There is no CHANGELOG in this repo, so the release-notes line for the same-path change is an action item at release time, not a file change. Flagged in the body. Two consecutive full runs: 3703 tests / 368 classes, 0 failures both times. detekt ktlintCheck clean - it caught two TooGenericExceptionCaught and a long line in this round's own code first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address project announcement review feedback * fix(plugin-store): restrict plugin_downloads RLS to the server-side pipeline (#488) * Batch validation: promote latest reviewed dev changes to main (#463) * Fix #30: Resolve duplicate context menu label collisions * Cleanup: Remove dead code for #91 and #93 * chore: Sanitize SubmitResult.Error at construction rather than at one render site * Fix panel component lifecycle disposal * fixes * Guard panel lifecycle destruction failures * fix(logging): sanitize secret RPC failures and crash-report errors Three related gaps in what gets logged and shown when something fails: - SecretService.kt had no BossLogger in any of its ten catch blocks (#145). When the organisation migration broke all four secret RPCs (#144), the only WARN in the log came from the calling plugin - the code that actually failed said nothing. Every catch now logs via logger.warn(LogCategory.NETWORK, ...), reusing the already-sanitized failure for both the log and the returned Result rather than sanitizing twice. RoleService and RoleCreationService were checked and already log correctly - SecretService was the only silent one. - LogSanitizer.sanitizeExceptionMessage redacted a hostname only when it appeared inside a URL - a bare hostname (exactly what UnknownHostException.getMessage() produces, i.e. every DNS failure, and what a proxy-connect failure looks like) passed through untouched (#109). Added a narrow hostname pattern: lowercase-only labels ending in a short explicit TLD/`.internal`/`.local` list, with a negative lookahead so a package path that happens to end in a real TLD word mid-FQN (kotlinx.coroutines.internal.ScopeCoroutine, kotlinx.io.EOFException) is not mistaken for one - caught by the existing realistic-stack-trace test after the first version of the pattern redacted a live Kotlin package name out of it. - CrashReportService.SubmitResult.Error held a raw exception string, sanitized at exactly one render call site in CrashReportDialog (#110). The constructor is now private; SubmitResult.Error.of(...) is the only way to build one, and it sanitizes before the raw string can reach .message - a property of the type now, not something every future consumer (a copy button, a toast, a log line) has to remember on its own. Both construction sites (the service's two catch blocks, and the one in CrashReportDialog the issue names directly) now route through it. Tests: new coverage for the ten now-logging SecretService paths (via SupabaseWiringTest's existing sanitization-wiring guard, updated to recognize the "sanitize once into a local, reuse for log and return" shape SupabaseDataProviderImpl already used), six new LogSanitizerTest cases for the hostname fix (including the FQN/version-number false-positive guards), and a new CrashReportServiceTest for the sanitize-at-construction guarantee. * test(crash): extract construction tests from #404 Selected unchanged test file from Antriksh1984 original commit 99d2a5228023c541e825fe2714619c8228140369 (PR #404). Maintainer extraction only; subsequent API/scope adaptations are recorded separately. * chore(ipc): clarify advisory menu ID scope and fix formatting * chore(ui): finish obsolete toolbar comment cleanup * test(crash): adapt consolidated coverage and guard factory visibility Maintainer consolidation: use #311 companion invoke and a URL fixture instead of depending on #404 hostname redaction. Add private constructor/copy bytecode assertions. Preserve original contributor tests and implementation credit separately. * refactor(crash): consolidate construction sanitization in #311 Maintainer scope change: remove only the duplicate #110 implementation, retaining Antriksh1984 original #109/#145 work and commit history. Standalone construction tests are retained with attribution in #311; its Aditya8369 implementation supersedes Error.of. Keep render-time sanitization here until #311 merges and describe the remaining hostname coverage conservatively. * 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> * 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> * 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 reg…
…isa-labs-inc#709) * Respect horizontal scrollers and commit trackpad navigation on release * Fix swipe cancellation, observer lifecycle and terminal delivery * Publish prior contact termination for home swipe attribution * fix(kernel): require verified caller identity for the run-configuration bridge (#634) * fix(overlays): honor RTL and fractional placement (#477) * 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> * 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(db): close signed-in crypto access and verify visibility boundaries * test(db): exercise repeated key rotation and exact anonymous ACLs * test(db): grant fixture role membership on Supabase Postgres * test(db): inline rotation source for isolated pgTAP mounts * test(db): keep generated rotation SQL out of source control * fix(db): reserve identity-taking store mutators for the edge service * fix(db): serialize rotations without requiring direct Vault writes * fix(db): address review gaps in system-org and rotation coverage * test(db): verify core secrets across hex and broker-free rotations * test(db): execute standing audit and missing-signature regression * fix(db): enforce revoke postconditions and harden rotation verification * test(db): enable recovery metadata in the authenticated fixture * test(db): use the schema-supported authenticator type * Address remaining database review quality findings * Fix invoker view audit boolean parsing and test timestamp bounds * Rotate the TOTP envelope, exempt extension routines, and close the repo/prod drift Addresses the handoff's database-integration item and the substantiated findings from the current-head review. Everything below was executed against the PR's own Supabase preview branch, not asserted from reading. **The blocker: rotation refused to run once #417 landed.** The guard was a blanket "TOTP is installed, refuse everything". It is now an adapter. The stored form is 'v1:' || encrypt_text(...) - same cipher, same key, only the framing differs - so the column map gained an envelope prefix and rotates like the rest once the prefix is stripped and re-applied. Three things this needed that reading the diff would not have shown: * #417's trigger RAISES on a v1: value ('TOTP input must be plaintext, not a storage envelope'), so the re-encryption update is rejected outright. The reviewed trigger is now disabled for the update and restored after, inside the same transaction. * safe_decrypt_recovery_codes returns jsonb, not text, so verification through the real read path needed a cast. Both fingerprints now go through each column's OWN application read path rather than decrypt_text, which also removes the step-1/step-5 asymmetry raised as U5. * a step 0 pre-check reports rows that are already unreadable, instead of letting a safe_decrypt_* NULL surface at the end as "a row was missed". The rejection contract is preserved, not removed: an unknown envelope version, an unmapped safe_decrypt_* wrapper, or an unreviewed BEFORE trigger on a mapped table each still refuse. All three are now regression-tested, along with the recovery-code column that was previously never exercised (its fixture was vacuous - create_secret writes no secret_metadata row without p_twofa_enabled). Verified: three consecutive rotations, TOTP/recovery/password all intact, v1 envelope preserved, trigger re-enabled and still enforcing its own contract. **CREATE EXTENSION was impossible.** The fail-closed guard aborted the first routine of any extension installed into public: `create extension pgtap` failed with "Anonymous EXECUTE remains on public.pg_version()". A guard that forces operators to disable it in order to install an extension is a guard that ends up disabled. Extension-owned routines are now exempted with a warning, and the exposure stays visible as advisory CHECK 1x rather than vanishing - it reports pgtap (1079) on the preview. The sweep skips them too: pgcrypto-style helpers are called from column DEFAULTs and CHECK constraints, which are evaluated with the DML role's privileges. No extension owns a function in public on a clean database, so no deployed behaviour changes. **The repo was weaker than production.** find_user_by_email, get_session_status and update_api_key_last_used were locked down on the live project on 2026-09-08 but never captured as a migration, so a fresh deploy re-opened them. find_user_by_email is a user-enumeration oracle over auth.users and signup is open. The standing audit caught this itself on the preview - CHECK 2 named it - which is the drift that check exists for. Both remaining callers use SUPABASE_SERVICE_ROLE_KEY, verified on this branch, so service_role keeps EXECUTE and neither path changes. Also: the guard no longer hard-fails on a database without the Supabase roles (a plain restore), and the vault fixtures no longer assume the key is absent (U3) - both suites failed outright on any database that already had one. Retained-key assertions measure this run's delta instead of an absolute count. Suites on the preview: rotation 16/16, db access audit, and visibility 54/54 all clean; the standing audit reports no failing gated check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Exercise the anonymous read path, not just the grant The keep-list assertions only checked has_function_privilege for the seven allowlisted signatures. That proves the grant, not the path: an RLS policy expression is evaluated as the QUERYING role, so revoking a policy helper turns an anonymous SELECT into 'permission denied for function ...' rather than an empty result, and no privilege assertion can see it. The seeding is the part that matters. A policy expression runs PER ROW, so on an empty table it is never evaluated - and plugins and user_roles are both empty on a fresh database, which made the first version of this probe pass no matter what had been revoked. Caught by trying to make it fail. Sensitivity is stated from measurement, not assumption: with a row present, revoking can_view_plugin_row from anon does fail the probe and revoking authorize does not, because permissive policies are ORed and short-circuit. A helper in a policy that never has to be evaluated cannot be detected this way by any test, so the comment says so rather than implying broader coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the gated audit checks and the test generator provable Both halves of this are the same failure: a suite that is green while proving nothing. I hit that twice writing the previous commits - a recovery-code fixture over a secret_metadata row create_secret never wrote, and an anonymous-read probe over an empty table whose RLS policy therefore never evaluated - so these are not hypothetical. Finding 6. The four CI-GATED checks (1, 1b, 4, 5) only ever asserted HEALTHY, while every advisory check already had a fixture-based detection test. An inverted `not exists`, a wrong role literal or a typo in the evtenabled test would have left the gate permanently green. Each is now broken deliberately and asserted to report it: a routine granted to anon AFTER creation (which is how one gets past the event trigger), a revoked deliberate anonymous grant, a disabled event trigger, and a crypto routine exposed to authenticated. They have teeth by construction - the assertion is that the finding appears, so a broken audit query fails them. Finding 8. The generator gained --check, which regenerates in memory and compares without writing, and orphan removal for a generated suite whose .sql.in has been renamed or deleted - that file otherwise keeps being executed forever with stale content. Orphans are identified by a provenance banner rather than a hard-coded list, so a hand-written suite is never touched; verified that explicit_anon_and_org_visibility_test.sql is untouched. Each guard was proven by making it fire: a hand edit and a planted orphan are both reported by --check, and generate removes the orphan. CI now runs generate and --check as their own step, so a preparation failure is visible in the log instead of being attributed to the test run. Preview: rotation 16/16, db access audit 15/15, visibility 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Announce every project change, not only the plugin-initiated ones Panels could come up empty and stay empty, and the reason was that the startup restore told no one. ProjectChangeEvent was published from ProjectDataProviderImpl.selectProject, which is the path a PLUGIN takes. The two callers that matter most bypass it: WorkspaceApplier.applyWorkspace calls windowProjectState.selectProject directly when it restores the workspace, and BossTopBar's picker goes through selectProjectInWindow. Neither published anything. The ordering that produced the empty panel: DefaultPlugin is constructed in a DisposableEffect keyed on registries and window state, not on selectedProject, and it kicks off the plugin scan asynchronously while BossAppScaffold composes and the sidebar builds panels immediately. The restore meanwhile waits on WorkspaceManager's sequential Dispatchers.IO JSON reads. When those reads are slow the panel is built first, reads the "" that WindowProjectState seeds itself with, and renders its no-project state. The restore then lands, silently, and nothing remounts the cached component. Publishing from the state itself rather than from one caller catches all three. The bus is MutableSharedFlow(replay = 0), so a publish that never happens cannot be recovered by a later subscriber - which is why this could not stay per-caller. The publish is removed from selectProject rather than added alongside the collector; keeping both would double-fire on the plugin path. previousPath is seeded from the current value so the StateFlow's replay of it is not announced as a change. That seed is "" at startup, and telling every plugin the project just became "" moments before the real restore arrives is precisely the clear-yourself signal worth avoiding. One collector per window: projectDataProvider is `by lazy` on DefaultPlugin, which is per-window, so this cannot stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Publish from the state's own callback, not from a lazily-built provider The review's finding 1: observing `selectedProject` from `ProjectDataProviderImpl` put the fix behind two `by lazy` initializers. `projectDataProvider` and `applicationEventBus` are both lazy on `DefaultPlugin`, and the second is the only host path that creates the bus at all. So the collector started only if some plugin touched both before the workspace JSON reads finished - and "the reads are slow" is the premise of the bug. Touched after the restore, the collector seeds to the restored path and announces nothing; never touched, no caller is announced at all. The announcement now hangs off the `ProjectSelectionCallback` that `WindowProjectStateRegistry` installs when it builds the window's state, which `BossAppState` does eagerly in composition. `WindowProjectState.selectProject` is the sole mutator of the selection and invokes that callback synchronously, so every caller is covered with no coroutine, no scope and nothing lazy in the path. Being synchronous also makes `previousProjectPath` a faithful history rather than best-effort: there is no StateFlow conflation to collapse A -> B -> C into A -> C. The two registry entry points carried a copy of the wiring each; they now share one `newState`, so the announcement cannot be installed on some windows and not others. Finding 1's third mitigation, which is a hole of its own: `publishSystemEvent` was a no-op whenever the bus had not been created, so on a build where no installed plugin had touched `applicationEventBus` yet, no host event existed - not `ProjectChangeEvent`, not `AuthEvent`, not `TabEvent` - and with `replay = 0` none could be recovered. It now creates the bus instead of dropping. `getInstance` also re-checks the registry on every call rather than only at creation, so an instance that exists while the registry is empty can no longer strand the host's publisher. Findings 2 and 3: `ProjectDataProviderImpl` implements `DisposableProvider` and `DefaultPlugin.dispose()` releases it through a named delegate, alongside `logDataProviderDelegate` and `gitDataProviderDelegate`; its scope gets a `SupervisorJob`, matching `pluginScope`. Tests: `ProjectChangeAnnouncementTest` (6) pins a direct `selectProject` being announced, the previous-path chain, the seeding rule, the same-path suppression, both registry entry points, and the bus being created rather than dropping the event. Full `:composeApp:desktopTest` green: 3695 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 2: honest bus wording, atomic previousPath, three more tests Review (1/2) and (2/2). 1. `publishSystemEvent` does not save the event it creates the bus for. Correct - no registered publisher means nobody holds the bus, so it has no subscribers and that first event still reaches no one. The comment, the test name and the PR body all claimed otherwise. Reworded to what it actually buys: the host stops being permanently silent while it waits for a plugin to touch `applicationEventBus`. 3. `previousPath` was check-then-act on a plain field, and `ProjectDataServiceBridge.selectProject` is a suspend gRPC handler with no hop to Main (verified: `KernelBootstrap` registers it, the bridge calls `provider.selectProject` directly). Now an `AtomicReference.getAndSet`, so the read and the write are one operation. 5. The registry read-modify-write moved back inside `synchronized(this)`. Both fields are in fact `@Volatile` in boss-plugin-api 1.0.87 (checked the pinned jar), but the lock is uncontended after the first call and removes the question. The bus-before-publisher ordering is now written down, including why a publish landing in that window loses nothing. 6. `initialPath` KDoc no longer describes a production scenario it cannot reach; it is documented as defensive and test-reachable, and defaulted to "". 7. Three tests added: - a plugin-initiated selection is announced exactly once (catches both a silent regression to zero and an accidentally re-added publish); - the registry test now asserts the recent-projects half of the callback ran - deleting it from `newState` previously left every test green; - `dispose()` stops the recent-projects collector, which is the whole reason `projectDataProviderDelegate` became a named lazy. `Dispatchers.setMain(UnconfinedTestDispatcher())` for the class: the provider collects on `Dispatchers.Main`, which has no implementation in a plain test JVM, so its collector silently never ran. Unconfined also makes the dispose test deterministic rather than a sleep. Smaller: `SupervisorJob` comment describes it as future-proofing (there is one collector); `systemEventBusScope` is `by lazy`; the KERNEL-mode consequence of disposing the provider is documented on `dispose()`. Not taken: `replay = 1` for `ProjectChangeEvent`. It would close the mirror race, but it changes `events()` semantics for every subscriber and every event type on a bus whose consumers are out of tree. The body now says the race is narrowed rather than closed, and names the api-side `selectedProject` StateFlow as what closes it. `:composeApp:desktopTest` 3697 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 3: unconditional announcement, ordered publish, drop dead register Review 3. 2. AGENTS.md: a bullet in the canonical "what a third-party plugin can observe" paragraph, since this widens *when* a filesystem path reaches every installed plugin. 3. The `expect` KDoc still said "a no-op if the bus has not been created yet". Only the `actual` had been corrected. Fixed, and it now states the part that IS still best-effort: replay = 0 means an unsubscribed event is gone either way. 4. The announcement sat downstream of `ProjectState.updateRecentProjects`, so a throw there skipped it AND left `previousPath` stale - every later selection reporting a previous path one step behind, silently, for the session. Now `try { recents } finally { announce }`: order preserved for anyone reading recents off the event, the announcement unconditional, the throw still propagating. 5. `getAndSet` closed the read-modify-write but not the publish order, so two concurrent selections could emit out of order. `previousPath` and the publish are now in one `synchronized` block (the AtomicReference is gone - the lock subsumes it). The KDoc says plainly what this still does not settle: `WindowProjectState` writes `_selectedProject.value` then calls the callback with no atomicity, so state and last event can still disagree. Closing that means locking upstream. 6. `ProjectState`'s async `loadRecentProjects()` assigns `_recentProjects.value` wholesale and could land mid-test, dropping a just-added path - only where `~/.boss/recent-projects.json` exists, so a developer-machine-only flake. A once-per-JVM settle in `@BeforeTest` puts that single file read before any test. The test paths stay deliberately non-existent, now with a comment saying why: the fire-and-forget saves are unordered, and `loadRecentProjects` reclaims entries whose directory is gone, so a real temp directory would leak where these self-heal. 7. `register` deleted. No production caller (`BossAppState:299` uses `getOrCreate`), and it overwrote an existing entry - handing back a state with a fresh announcer seeded to "" for a window that already had a project. `newState` stays; it is what keeps the wiring from drifting. 8. `@Volatile` dropped from `ApplicationEventBusImpl.instance` - every access is inside the lock now, and leaving it reads as if a fast path survives. Also: a test for the actual regression path, `applyWorkspace` announcing the project it restores, rather than a comment claiming to imitate it. Answering the grep you could not run: the only `projectChanges()` consumers in boss_plugins are the two fluck-agent panels (`FluckAgentViewModel`), and both assign `_bossProject.value` and re-sweep - idempotent on a repeat. Nothing used the repeat publish as a reload nudge. Recorded in the announcer comment. Item 1 was already in the PR body from round 2 ("The startup race is narrowed, not closed"), naming the mirror case and the api-side StateFlow that closes it. `:composeApp:desktopTest` 3698 tests / 368 classes, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 4: split the dispose out, stop tests writing the real ~/.boss Review 4. Items 1 and 3 were the before-merge ones. 1. Verified: ProjectDataServiceBridge.watchRecentProjects collects provider.recentProjects, a StateFlow, so cancelling the provider's scope on window close leaves that gRPC stream open and silently frozen for every out-of-process plugin. Taking the first option and SPLITTING the dispose out. Note this reverses what round 1 asked for, and the justification changed rather than the opinion: once the announcement moved to the registry callback, ProjectDataProviderImpl went back to being exactly what it was before this PR, so the leak is pre-existing and unrelated to the subject. Closing it properly means the bridge reading ProjectState directly instead of a per-window provider, which is its own change. The reasoning is on the class so it is not re-fixed by accident. 3. Verified and worse than described: MAX_RECENT_PROJECTS = 10, so test entries evict real ones from the developer's picker and no cleanup restores them. systemProperty("user.home", <build>/test-home) on the Test task. One run with it in place shows the existing suite was writing far more than recents to the real home - window-appearance-settings.json, keymap-settings.json, scrollbar-settings.json, recent-browser-pages.json, dashboard-stats.json and a ~/BossProjects/ directory all land in test-home now. The settle is gone with it: no recent-projects.json in test-home means the wholesale reload never happens. 2. publishSystemEvent now refuses the bus != null && systemPublisher == null state instead of falling through into a bus that may not be the registry's, with one warning rather than one per event. The comment at the fallback is true as written. 4. Both stale comments fixed (one construction path, not two). 5. hostProjectCallback(updateRecents, announcer) extracted so the try/finally claim is testable - ProjectState is an object, so a hard-coded call cannot be made to fail. 6. The publish-inside-the-lock trade is spelled out in the KDoc: what it buys (ordering), what it costs (an inline subscriber runs holding the lock inside selectProject), and that the contended path is untested. 7. The serial-execution dependency is recorded in the test class KDoc. Plus: multi-window isolation, and a test that makes the recents update throw. One thing this round caused and fixed: removing dispose() left two ProjectDataProviderImpl collectors alive on the test dispatcher. On a TestCoroutineScheduler a live coroutine is something every later runTest waits on, and the git classes timed out at 60s in full-suite runs while passing in isolation. Dispatchers.setMain(Dispatchers.Unconfined) rather than UnconfinedTestDispatcher(): same inline behaviour, not enrolled in a scheduler anyone else observes. It is also a real argument for the follow-up - without dispose() the provider cannot be constructed in a test without leaking. :composeApp:desktopTest 3699 tests / 368 classes, 0 failures. detekt ktlintCheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 5: reset test-home per run, stop getInstance stealing a publisher Review 5. 1. Confirmed reproducibly - build/test-home/.boss/recent-projects.json was sitting there from the previous run. doFirst now deleteRecursively() before mkdirs(), so the home is fresh per run rather than merely private. Without it the redirect only helped the first run and ProjectState.init's load reintroduced the same race from the second. Verified by running the full suite twice back to back. 2. Right, and sharper than it looks: the guard tested `bus` but wrote both fields, and `systemPublisher != null && bus == null` is exactly what ProjectChangeAnnouncementTest, BrowserAnalyticsEmissionTest and BossTabsComponentMoveTest install. Guard now tests both, so getInstance cannot take a publisher away from whoever set it. 3. Test added for the warn-and-drop branch, asserting the DROP rather than the warning - partialRegistryWarned is a one-shot process global, so "it warns" is unassertable after the first test to trip it. Uses a stub bus so the half-registry state is real. 4. The re-entrancy hazard is now documented as SAFE and why (synchronized is reentrant, previousPath advances before the publish), so it is not "fixed" later. The lock-widens-a-UI-hang-to-a-cross-thread-one point is in the same block. 5. Taken, including the optional ones: - hostProjectCallback keeps both failures (addSuppressed) instead of letting finally discard the first. Needs @Suppress("TooGenericExceptionCaught") - catching Throwable is the contract, and both are rethrown. - ProjectDataProviderImpl takes an injectable dispatcher (default Dispatchers.Main). This removes the global setMain AND the round-4 collector leak at the source rather than working around them, without reopening the DisposableProvider split. - Three more pins: a null window state announces nothing, applyWorkspace (restoreProject = false) announces nothing, unregister + getOrCreate restarts the chain at "". Not taken, with reasons: jvmArgumentProviders (the current form works, is config-cache-serialisable and passed CI on three OSes); widening the user.home redirect to plugin-path-utils (its test only mkdirs a directory every BOSS user already has). There is no CHANGELOG in this repo, so the release-notes line for the same-path change is an action item at release time, not a file change. Flagged in the body. Two consecutive full runs: 3703 tests / 368 classes, 0 failures both times. detekt ktlintCheck clean - it caught two TooGenericExceptionCaught and a long line in this round's own code first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address project announcement review feedback * fix(plugin-store): restrict plugin_downloads RLS to the server-side pipeline (#488) * Batch validation: promote latest reviewed dev changes to main (#463) * Fix #30: Resolve duplicate context menu label collisions * Cleanup: Remove dead code for #91 and #93 * chore: Sanitize SubmitResult.Error at construction rather than at one render site * Fix panel component lifecycle disposal * fixes * Guard panel lifecycle destruction failures * fix(logging): sanitize secret RPC failures and crash-report errors Three related gaps in what gets logged and shown when something fails: - SecretService.kt had no BossLogger in any of its ten catch blocks (#145). When the organisation migration broke all four secret RPCs (#144), the only WARN in the log came from the calling plugin - the code that actually failed said nothing. Every catch now logs via logger.warn(LogCategory.NETWORK, ...), reusing the already-sanitized failure for both the log and the returned Result rather than sanitizing twice. RoleService and RoleCreationService were checked and already log correctly - SecretService was the only silent one. - LogSanitizer.sanitizeExceptionMessage redacted a hostname only when it appeared inside a URL - a bare hostname (exactly what UnknownHostException.getMessage() produces, i.e. every DNS failure, and what a proxy-connect failure looks like) passed through untouched (#109). Added a narrow hostname pattern: lowercase-only labels ending in a short explicit TLD/`.internal`/`.local` list, with a negative lookahead so a package path that happens to end in a real TLD word mid-FQN (kotlinx.coroutines.internal.ScopeCoroutine, kotlinx.io.EOFException) is not mistaken for one - caught by the existing realistic-stack-trace test after the first version of the pattern redacted a live Kotlin package name out of it. - CrashReportService.SubmitResult.Error held a raw exception string, sanitized at exactly one render call site in CrashReportDialog (#110). The constructor is now private; SubmitResult.Error.of(...) is the only way to build one, and it sanitizes before the raw string can reach .message - a property of the type now, not something every future consumer (a copy button, a toast, a log line) has to remember on its own. Both construction sites (the service's two catch blocks, and the one in CrashReportDialog the issue names directly) now route through it. Tests: new coverage for the ten now-logging SecretService paths (via SupabaseWiringTest's existing sanitization-wiring guard, updated to recognize the "sanitize once into a local, reuse for log and return" shape SupabaseDataProviderImpl already used), six new LogSanitizerTest cases for the hostname fix (including the FQN/version-number false-positive guards), and a new CrashReportServiceTest for the sanitize-at-construction guarantee. * test(crash): extract construction tests from #404 Selected unchanged test file from Antriksh1984 original commit 99d2a5228023c541e825fe2714619c8228140369 (PR #404). Maintainer extraction only; subsequent API/scope adaptations are recorded separately. * chore(ipc): clarify advisory menu ID scope and fix formatting * chore(ui): finish obsolete toolbar comment cleanup * test(crash): adapt consolidated coverage and guard factory visibility Maintainer consolidation: use #311 companion invoke and a URL fixture instead of depending on #404 hostname redaction. Add private constructor/copy bytecode assertions. Preserve original contributor tests and implementation credit separately. * refactor(crash): consolidate construction sanitization in #311 Maintainer scope change: remove only the duplicate #110 implementation, retaining Antriksh1984 original #109/#145 work and commit history. Standalone construction tests are retained with attribution in #311; its Aditya8369 implementation supersedes Error.of. Keep render-time sanitization here until #311 merges and describe the remaining hostname coverage conservatively. * 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> * 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> * 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 registra…
…d but discarded (risa-labs-inc#712) * Respect horizontal scrollers and commit trackpad navigation on release * Fix swipe cancellation, observer lifecycle and terminal delivery * Publish prior contact termination for home swipe attribution * fix(kernel): require verified caller identity for the run-configuration bridge (#634) * fix(overlays): honor RTL and fractional placement (#477) * 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> * 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(db): close signed-in crypto access and verify visibility boundaries * test(db): exercise repeated key rotation and exact anonymous ACLs * test(db): grant fixture role membership on Supabase Postgres * test(db): inline rotation source for isolated pgTAP mounts * test(db): keep generated rotation SQL out of source control * fix(db): reserve identity-taking store mutators for the edge service * fix(db): serialize rotations without requiring direct Vault writes * fix(db): address review gaps in system-org and rotation coverage * test(db): verify core secrets across hex and broker-free rotations * test(db): execute standing audit and missing-signature regression * fix(db): enforce revoke postconditions and harden rotation verification * test(db): enable recovery metadata in the authenticated fixture * test(db): use the schema-supported authenticator type * Address remaining database review quality findings * Fix invoker view audit boolean parsing and test timestamp bounds * Rotate the TOTP envelope, exempt extension routines, and close the repo/prod drift Addresses the handoff's database-integration item and the substantiated findings from the current-head review. Everything below was executed against the PR's own Supabase preview branch, not asserted from reading. **The blocker: rotation refused to run once #417 landed.** The guard was a blanket "TOTP is installed, refuse everything". It is now an adapter. The stored form is 'v1:' || encrypt_text(...) - same cipher, same key, only the framing differs - so the column map gained an envelope prefix and rotates like the rest once the prefix is stripped and re-applied. Three things this needed that reading the diff would not have shown: * #417's trigger RAISES on a v1: value ('TOTP input must be plaintext, not a storage envelope'), so the re-encryption update is rejected outright. The reviewed trigger is now disabled for the update and restored after, inside the same transaction. * safe_decrypt_recovery_codes returns jsonb, not text, so verification through the real read path needed a cast. Both fingerprints now go through each column's OWN application read path rather than decrypt_text, which also removes the step-1/step-5 asymmetry raised as U5. * a step 0 pre-check reports rows that are already unreadable, instead of letting a safe_decrypt_* NULL surface at the end as "a row was missed". The rejection contract is preserved, not removed: an unknown envelope version, an unmapped safe_decrypt_* wrapper, or an unreviewed BEFORE trigger on a mapped table each still refuse. All three are now regression-tested, along with the recovery-code column that was previously never exercised (its fixture was vacuous - create_secret writes no secret_metadata row without p_twofa_enabled). Verified: three consecutive rotations, TOTP/recovery/password all intact, v1 envelope preserved, trigger re-enabled and still enforcing its own contract. **CREATE EXTENSION was impossible.** The fail-closed guard aborted the first routine of any extension installed into public: `create extension pgtap` failed with "Anonymous EXECUTE remains on public.pg_version()". A guard that forces operators to disable it in order to install an extension is a guard that ends up disabled. Extension-owned routines are now exempted with a warning, and the exposure stays visible as advisory CHECK 1x rather than vanishing - it reports pgtap (1079) on the preview. The sweep skips them too: pgcrypto-style helpers are called from column DEFAULTs and CHECK constraints, which are evaluated with the DML role's privileges. No extension owns a function in public on a clean database, so no deployed behaviour changes. **The repo was weaker than production.** find_user_by_email, get_session_status and update_api_key_last_used were locked down on the live project on 2026-09-08 but never captured as a migration, so a fresh deploy re-opened them. find_user_by_email is a user-enumeration oracle over auth.users and signup is open. The standing audit caught this itself on the preview - CHECK 2 named it - which is the drift that check exists for. Both remaining callers use SUPABASE_SERVICE_ROLE_KEY, verified on this branch, so service_role keeps EXECUTE and neither path changes. Also: the guard no longer hard-fails on a database without the Supabase roles (a plain restore), and the vault fixtures no longer assume the key is absent (U3) - both suites failed outright on any database that already had one. Retained-key assertions measure this run's delta instead of an absolute count. Suites on the preview: rotation 16/16, db access audit, and visibility 54/54 all clean; the standing audit reports no failing gated check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Exercise the anonymous read path, not just the grant The keep-list assertions only checked has_function_privilege for the seven allowlisted signatures. That proves the grant, not the path: an RLS policy expression is evaluated as the QUERYING role, so revoking a policy helper turns an anonymous SELECT into 'permission denied for function ...' rather than an empty result, and no privilege assertion can see it. The seeding is the part that matters. A policy expression runs PER ROW, so on an empty table it is never evaluated - and plugins and user_roles are both empty on a fresh database, which made the first version of this probe pass no matter what had been revoked. Caught by trying to make it fail. Sensitivity is stated from measurement, not assumption: with a row present, revoking can_view_plugin_row from anon does fail the probe and revoking authorize does not, because permissive policies are ORed and short-circuit. A helper in a policy that never has to be evaluated cannot be detected this way by any test, so the comment says so rather than implying broader coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the gated audit checks and the test generator provable Both halves of this are the same failure: a suite that is green while proving nothing. I hit that twice writing the previous commits - a recovery-code fixture over a secret_metadata row create_secret never wrote, and an anonymous-read probe over an empty table whose RLS policy therefore never evaluated - so these are not hypothetical. Finding 6. The four CI-GATED checks (1, 1b, 4, 5) only ever asserted HEALTHY, while every advisory check already had a fixture-based detection test. An inverted `not exists`, a wrong role literal or a typo in the evtenabled test would have left the gate permanently green. Each is now broken deliberately and asserted to report it: a routine granted to anon AFTER creation (which is how one gets past the event trigger), a revoked deliberate anonymous grant, a disabled event trigger, and a crypto routine exposed to authenticated. They have teeth by construction - the assertion is that the finding appears, so a broken audit query fails them. Finding 8. The generator gained --check, which regenerates in memory and compares without writing, and orphan removal for a generated suite whose .sql.in has been renamed or deleted - that file otherwise keeps being executed forever with stale content. Orphans are identified by a provenance banner rather than a hard-coded list, so a hand-written suite is never touched; verified that explicit_anon_and_org_visibility_test.sql is untouched. Each guard was proven by making it fire: a hand edit and a planted orphan are both reported by --check, and generate removes the orphan. CI now runs generate and --check as their own step, so a preparation failure is visible in the log instead of being attributed to the test run. Preview: rotation 16/16, db access audit 15/15, visibility 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Announce every project change, not only the plugin-initiated ones Panels could come up empty and stay empty, and the reason was that the startup restore told no one. ProjectChangeEvent was published from ProjectDataProviderImpl.selectProject, which is the path a PLUGIN takes. The two callers that matter most bypass it: WorkspaceApplier.applyWorkspace calls windowProjectState.selectProject directly when it restores the workspace, and BossTopBar's picker goes through selectProjectInWindow. Neither published anything. The ordering that produced the empty panel: DefaultPlugin is constructed in a DisposableEffect keyed on registries and window state, not on selectedProject, and it kicks off the plugin scan asynchronously while BossAppScaffold composes and the sidebar builds panels immediately. The restore meanwhile waits on WorkspaceManager's sequential Dispatchers.IO JSON reads. When those reads are slow the panel is built first, reads the "" that WindowProjectState seeds itself with, and renders its no-project state. The restore then lands, silently, and nothing remounts the cached component. Publishing from the state itself rather than from one caller catches all three. The bus is MutableSharedFlow(replay = 0), so a publish that never happens cannot be recovered by a later subscriber - which is why this could not stay per-caller. The publish is removed from selectProject rather than added alongside the collector; keeping both would double-fire on the plugin path. previousPath is seeded from the current value so the StateFlow's replay of it is not announced as a change. That seed is "" at startup, and telling every plugin the project just became "" moments before the real restore arrives is precisely the clear-yourself signal worth avoiding. One collector per window: projectDataProvider is `by lazy` on DefaultPlugin, which is per-window, so this cannot stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Publish from the state's own callback, not from a lazily-built provider The review's finding 1: observing `selectedProject` from `ProjectDataProviderImpl` put the fix behind two `by lazy` initializers. `projectDataProvider` and `applicationEventBus` are both lazy on `DefaultPlugin`, and the second is the only host path that creates the bus at all. So the collector started only if some plugin touched both before the workspace JSON reads finished - and "the reads are slow" is the premise of the bug. Touched after the restore, the collector seeds to the restored path and announces nothing; never touched, no caller is announced at all. The announcement now hangs off the `ProjectSelectionCallback` that `WindowProjectStateRegistry` installs when it builds the window's state, which `BossAppState` does eagerly in composition. `WindowProjectState.selectProject` is the sole mutator of the selection and invokes that callback synchronously, so every caller is covered with no coroutine, no scope and nothing lazy in the path. Being synchronous also makes `previousProjectPath` a faithful history rather than best-effort: there is no StateFlow conflation to collapse A -> B -> C into A -> C. The two registry entry points carried a copy of the wiring each; they now share one `newState`, so the announcement cannot be installed on some windows and not others. Finding 1's third mitigation, which is a hole of its own: `publishSystemEvent` was a no-op whenever the bus had not been created, so on a build where no installed plugin had touched `applicationEventBus` yet, no host event existed - not `ProjectChangeEvent`, not `AuthEvent`, not `TabEvent` - and with `replay = 0` none could be recovered. It now creates the bus instead of dropping. `getInstance` also re-checks the registry on every call rather than only at creation, so an instance that exists while the registry is empty can no longer strand the host's publisher. Findings 2 and 3: `ProjectDataProviderImpl` implements `DisposableProvider` and `DefaultPlugin.dispose()` releases it through a named delegate, alongside `logDataProviderDelegate` and `gitDataProviderDelegate`; its scope gets a `SupervisorJob`, matching `pluginScope`. Tests: `ProjectChangeAnnouncementTest` (6) pins a direct `selectProject` being announced, the previous-path chain, the seeding rule, the same-path suppression, both registry entry points, and the bus being created rather than dropping the event. Full `:composeApp:desktopTest` green: 3695 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 2: honest bus wording, atomic previousPath, three more tests Review (1/2) and (2/2). 1. `publishSystemEvent` does not save the event it creates the bus for. Correct - no registered publisher means nobody holds the bus, so it has no subscribers and that first event still reaches no one. The comment, the test name and the PR body all claimed otherwise. Reworded to what it actually buys: the host stops being permanently silent while it waits for a plugin to touch `applicationEventBus`. 3. `previousPath` was check-then-act on a plain field, and `ProjectDataServiceBridge.selectProject` is a suspend gRPC handler with no hop to Main (verified: `KernelBootstrap` registers it, the bridge calls `provider.selectProject` directly). Now an `AtomicReference.getAndSet`, so the read and the write are one operation. 5. The registry read-modify-write moved back inside `synchronized(this)`. Both fields are in fact `@Volatile` in boss-plugin-api 1.0.87 (checked the pinned jar), but the lock is uncontended after the first call and removes the question. The bus-before-publisher ordering is now written down, including why a publish landing in that window loses nothing. 6. `initialPath` KDoc no longer describes a production scenario it cannot reach; it is documented as defensive and test-reachable, and defaulted to "". 7. Three tests added: - a plugin-initiated selection is announced exactly once (catches both a silent regression to zero and an accidentally re-added publish); - the registry test now asserts the recent-projects half of the callback ran - deleting it from `newState` previously left every test green; - `dispose()` stops the recent-projects collector, which is the whole reason `projectDataProviderDelegate` became a named lazy. `Dispatchers.setMain(UnconfinedTestDispatcher())` for the class: the provider collects on `Dispatchers.Main`, which has no implementation in a plain test JVM, so its collector silently never ran. Unconfined also makes the dispose test deterministic rather than a sleep. Smaller: `SupervisorJob` comment describes it as future-proofing (there is one collector); `systemEventBusScope` is `by lazy`; the KERNEL-mode consequence of disposing the provider is documented on `dispose()`. Not taken: `replay = 1` for `ProjectChangeEvent`. It would close the mirror race, but it changes `events()` semantics for every subscriber and every event type on a bus whose consumers are out of tree. The body now says the race is narrowed rather than closed, and names the api-side `selectedProject` StateFlow as what closes it. `:composeApp:desktopTest` 3697 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 3: unconditional announcement, ordered publish, drop dead register Review 3. 2. AGENTS.md: a bullet in the canonical "what a third-party plugin can observe" paragraph, since this widens *when* a filesystem path reaches every installed plugin. 3. The `expect` KDoc still said "a no-op if the bus has not been created yet". Only the `actual` had been corrected. Fixed, and it now states the part that IS still best-effort: replay = 0 means an unsubscribed event is gone either way. 4. The announcement sat downstream of `ProjectState.updateRecentProjects`, so a throw there skipped it AND left `previousPath` stale - every later selection reporting a previous path one step behind, silently, for the session. Now `try { recents } finally { announce }`: order preserved for anyone reading recents off the event, the announcement unconditional, the throw still propagating. 5. `getAndSet` closed the read-modify-write but not the publish order, so two concurrent selections could emit out of order. `previousPath` and the publish are now in one `synchronized` block (the AtomicReference is gone - the lock subsumes it). The KDoc says plainly what this still does not settle: `WindowProjectState` writes `_selectedProject.value` then calls the callback with no atomicity, so state and last event can still disagree. Closing that means locking upstream. 6. `ProjectState`'s async `loadRecentProjects()` assigns `_recentProjects.value` wholesale and could land mid-test, dropping a just-added path - only where `~/.boss/recent-projects.json` exists, so a developer-machine-only flake. A once-per-JVM settle in `@BeforeTest` puts that single file read before any test. The test paths stay deliberately non-existent, now with a comment saying why: the fire-and-forget saves are unordered, and `loadRecentProjects` reclaims entries whose directory is gone, so a real temp directory would leak where these self-heal. 7. `register` deleted. No production caller (`BossAppState:299` uses `getOrCreate`), and it overwrote an existing entry - handing back a state with a fresh announcer seeded to "" for a window that already had a project. `newState` stays; it is what keeps the wiring from drifting. 8. `@Volatile` dropped from `ApplicationEventBusImpl.instance` - every access is inside the lock now, and leaving it reads as if a fast path survives. Also: a test for the actual regression path, `applyWorkspace` announcing the project it restores, rather than a comment claiming to imitate it. Answering the grep you could not run: the only `projectChanges()` consumers in boss_plugins are the two fluck-agent panels (`FluckAgentViewModel`), and both assign `_bossProject.value` and re-sweep - idempotent on a repeat. Nothing used the repeat publish as a reload nudge. Recorded in the announcer comment. Item 1 was already in the PR body from round 2 ("The startup race is narrowed, not closed"), naming the mirror case and the api-side StateFlow that closes it. `:composeApp:desktopTest` 3698 tests / 368 classes, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 4: split the dispose out, stop tests writing the real ~/.boss Review 4. Items 1 and 3 were the before-merge ones. 1. Verified: ProjectDataServiceBridge.watchRecentProjects collects provider.recentProjects, a StateFlow, so cancelling the provider's scope on window close leaves that gRPC stream open and silently frozen for every out-of-process plugin. Taking the first option and SPLITTING the dispose out. Note this reverses what round 1 asked for, and the justification changed rather than the opinion: once the announcement moved to the registry callback, ProjectDataProviderImpl went back to being exactly what it was before this PR, so the leak is pre-existing and unrelated to the subject. Closing it properly means the bridge reading ProjectState directly instead of a per-window provider, which is its own change. The reasoning is on the class so it is not re-fixed by accident. 3. Verified and worse than described: MAX_RECENT_PROJECTS = 10, so test entries evict real ones from the developer's picker and no cleanup restores them. systemProperty("user.home", <build>/test-home) on the Test task. One run with it in place shows the existing suite was writing far more than recents to the real home - window-appearance-settings.json, keymap-settings.json, scrollbar-settings.json, recent-browser-pages.json, dashboard-stats.json and a ~/BossProjects/ directory all land in test-home now. The settle is gone with it: no recent-projects.json in test-home means the wholesale reload never happens. 2. publishSystemEvent now refuses the bus != null && systemPublisher == null state instead of falling through into a bus that may not be the registry's, with one warning rather than one per event. The comment at the fallback is true as written. 4. Both stale comments fixed (one construction path, not two). 5. hostProjectCallback(updateRecents, announcer) extracted so the try/finally claim is testable - ProjectState is an object, so a hard-coded call cannot be made to fail. 6. The publish-inside-the-lock trade is spelled out in the KDoc: what it buys (ordering), what it costs (an inline subscriber runs holding the lock inside selectProject), and that the contended path is untested. 7. The serial-execution dependency is recorded in the test class KDoc. Plus: multi-window isolation, and a test that makes the recents update throw. One thing this round caused and fixed: removing dispose() left two ProjectDataProviderImpl collectors alive on the test dispatcher. On a TestCoroutineScheduler a live coroutine is something every later runTest waits on, and the git classes timed out at 60s in full-suite runs while passing in isolation. Dispatchers.setMain(Dispatchers.Unconfined) rather than UnconfinedTestDispatcher(): same inline behaviour, not enrolled in a scheduler anyone else observes. It is also a real argument for the follow-up - without dispose() the provider cannot be constructed in a test without leaking. :composeApp:desktopTest 3699 tests / 368 classes, 0 failures. detekt ktlintCheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 5: reset test-home per run, stop getInstance stealing a publisher Review 5. 1. Confirmed reproducibly - build/test-home/.boss/recent-projects.json was sitting there from the previous run. doFirst now deleteRecursively() before mkdirs(), so the home is fresh per run rather than merely private. Without it the redirect only helped the first run and ProjectState.init's load reintroduced the same race from the second. Verified by running the full suite twice back to back. 2. Right, and sharper than it looks: the guard tested `bus` but wrote both fields, and `systemPublisher != null && bus == null` is exactly what ProjectChangeAnnouncementTest, BrowserAnalyticsEmissionTest and BossTabsComponentMoveTest install. Guard now tests both, so getInstance cannot take a publisher away from whoever set it. 3. Test added for the warn-and-drop branch, asserting the DROP rather than the warning - partialRegistryWarned is a one-shot process global, so "it warns" is unassertable after the first test to trip it. Uses a stub bus so the half-registry state is real. 4. The re-entrancy hazard is now documented as SAFE and why (synchronized is reentrant, previousPath advances before the publish), so it is not "fixed" later. The lock-widens-a-UI-hang-to-a-cross-thread-one point is in the same block. 5. Taken, including the optional ones: - hostProjectCallback keeps both failures (addSuppressed) instead of letting finally discard the first. Needs @Suppress("TooGenericExceptionCaught") - catching Throwable is the contract, and both are rethrown. - ProjectDataProviderImpl takes an injectable dispatcher (default Dispatchers.Main). This removes the global setMain AND the round-4 collector leak at the source rather than working around them, without reopening the DisposableProvider split. - Three more pins: a null window state announces nothing, applyWorkspace (restoreProject = false) announces nothing, unregister + getOrCreate restarts the chain at "". Not taken, with reasons: jvmArgumentProviders (the current form works, is config-cache-serialisable and passed CI on three OSes); widening the user.home redirect to plugin-path-utils (its test only mkdirs a directory every BOSS user already has). There is no CHANGELOG in this repo, so the release-notes line for the same-path change is an action item at release time, not a file change. Flagged in the body. Two consecutive full runs: 3703 tests / 368 classes, 0 failures both times. detekt ktlintCheck clean - it caught two TooGenericExceptionCaught and a long line in this round's own code first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address project announcement review feedback * fix(plugin-store): restrict plugin_downloads RLS to the server-side pipeline (#488) * Batch validation: promote latest reviewed dev changes to main (#463) * Fix #30: Resolve duplicate context menu label collisions * Cleanup: Remove dead code for #91 and #93 * chore: Sanitize SubmitResult.Error at construction rather than at one render site * Fix panel component lifecycle disposal * fixes * Guard panel lifecycle destruction failures * fix(logging): sanitize secret RPC failures and crash-report errors Three related gaps in what gets logged and shown when something fails: - SecretService.kt had no BossLogger in any of its ten catch blocks (#145). When the organisation migration broke all four secret RPCs (#144), the only WARN in the log came from the calling plugin - the code that actually failed said nothing. Every catch now logs via logger.warn(LogCategory.NETWORK, ...), reusing the already-sanitized failure for both the log and the returned Result rather than sanitizing twice. RoleService and RoleCreationService were checked and already log correctly - SecretService was the only silent one. - LogSanitizer.sanitizeExceptionMessage redacted a hostname only when it appeared inside a URL - a bare hostname (exactly what UnknownHostException.getMessage() produces, i.e. every DNS failure, and what a proxy-connect failure looks like) passed through untouched (#109). Added a narrow hostname pattern: lowercase-only labels ending in a short explicit TLD/`.internal`/`.local` list, with a negative lookahead so a package path that happens to end in a real TLD word mid-FQN (kotlinx.coroutines.internal.ScopeCoroutine, kotlinx.io.EOFException) is not mistaken for one - caught by the existing realistic-stack-trace test after the first version of the pattern redacted a live Kotlin package name out of it. - CrashReportService.SubmitResult.Error held a raw exception string, sanitized at exactly one render call site in CrashReportDialog (#110). The constructor is now private; SubmitResult.Error.of(...) is the only way to build one, and it sanitizes before the raw string can reach .message - a property of the type now, not something every future consumer (a copy button, a toast, a log line) has to remember on its own. Both construction sites (the service's two catch blocks, and the one in CrashReportDialog the issue names directly) now route through it. Tests: new coverage for the ten now-logging SecretService paths (via SupabaseWiringTest's existing sanitization-wiring guard, updated to recognize the "sanitize once into a local, reuse for log and return" shape SupabaseDataProviderImpl already used), six new LogSanitizerTest cases for the hostname fix (including the FQN/version-number false-positive guards), and a new CrashReportServiceTest for the sanitize-at-construction guarantee. * test(crash): extract construction tests from #404 Selected unchanged test file from Antriksh1984 original commit 99d2a5228023c541e825fe2714619c8228140369 (PR #404). Maintainer extraction only; subsequent API/scope adaptations are recorded separately. * chore(ipc): clarify advisory menu ID scope and fix formatting * chore(ui): finish obsolete toolbar comment cleanup * test(crash): adapt consolidated coverage and guard factory visibility Maintainer consolidation: use #311 companion invoke and a URL fixture instead of depending on #404 hostname redaction. Add private constructor/copy bytecode assertions. Preserve original contributor tests and implementation credit separately. * refactor(crash): consolidate construction sanitization in #311 Maintainer scope change: remove only the duplicate #110 implementation, retaining Antriksh1984 original #109/#145 work and commit history. Standalone construction tests are retained with attribution in #311; its Aditya8369 implementation supersedes Error.of. Keep render-time sanitization here until #311 merges and describe the remaining hostname coverage conservatively. * 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> * 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> * 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 plug…
…isa-labs-inc#571) * fix: encode store endpoint segments and isolate analytics hashing key * test(review): disable auth refresh in download analytics fixture * fix(review): surface and document download hashing configuration --------- Co-authored-by: Raghav Bansal <273537979+Raghav-Bansal-15@users.noreply.github.com> Co-authored-by: Shivang <shivang.iitk@gmail.com>
…-labs-inc#565) * Address plugin cache entries by exact hashed identities * fix(cache): require matching identity metadata on lookup * fix(cache): keep cache failures from blocking verified downloads * fix(cache): retain successful root initialization for cache clients * ci: run full validation on security repair branches * fix(cache): handle cache refusals with explicit exception types * Respect horizontal scrollers and commit trackpad navigation on release * Fix swipe cancellation, observer lifecycle and terminal delivery * Publish prior contact termination for home swipe attribution * fix(kernel): require verified caller identity for the run-configuration bridge (#634) * fix(overlays): honor RTL and fractional placement (#477) * 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> * 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(db): close signed-in crypto access and verify visibility boundaries * test(db): exercise repeated key rotation and exact anonymous ACLs * test(db): grant fixture role membership on Supabase Postgres * test(db): inline rotation source for isolated pgTAP mounts * test(db): keep generated rotation SQL out of source control * fix(db): reserve identity-taking store mutators for the edge service * fix(db): serialize rotations without requiring direct Vault writes * fix(db): address review gaps in system-org and rotation coverage * test(db): verify core secrets across hex and broker-free rotations * test(db): execute standing audit and missing-signature regression * fix(db): enforce revoke postconditions and harden rotation verification * test(db): enable recovery metadata in the authenticated fixture * test(db): use the schema-supported authenticator type * Address remaining database review quality findings * Fix invoker view audit boolean parsing and test timestamp bounds * Rotate the TOTP envelope, exempt extension routines, and close the repo/prod drift Addresses the handoff's database-integration item and the substantiated findings from the current-head review. Everything below was executed against the PR's own Supabase preview branch, not asserted from reading. **The blocker: rotation refused to run once #417 landed.** The guard was a blanket "TOTP is installed, refuse everything". It is now an adapter. The stored form is 'v1:' || encrypt_text(...) - same cipher, same key, only the framing differs - so the column map gained an envelope prefix and rotates like the rest once the prefix is stripped and re-applied. Three things this needed that reading the diff would not have shown: * #417's trigger RAISES on a v1: value ('TOTP input must be plaintext, not a storage envelope'), so the re-encryption update is rejected outright. The reviewed trigger is now disabled for the update and restored after, inside the same transaction. * safe_decrypt_recovery_codes returns jsonb, not text, so verification through the real read path needed a cast. Both fingerprints now go through each column's OWN application read path rather than decrypt_text, which also removes the step-1/step-5 asymmetry raised as U5. * a step 0 pre-check reports rows that are already unreadable, instead of letting a safe_decrypt_* NULL surface at the end as "a row was missed". The rejection contract is preserved, not removed: an unknown envelope version, an unmapped safe_decrypt_* wrapper, or an unreviewed BEFORE trigger on a mapped table each still refuse. All three are now regression-tested, along with the recovery-code column that was previously never exercised (its fixture was vacuous - create_secret writes no secret_metadata row without p_twofa_enabled). Verified: three consecutive rotations, TOTP/recovery/password all intact, v1 envelope preserved, trigger re-enabled and still enforcing its own contract. **CREATE EXTENSION was impossible.** The fail-closed guard aborted the first routine of any extension installed into public: `create extension pgtap` failed with "Anonymous EXECUTE remains on public.pg_version()". A guard that forces operators to disable it in order to install an extension is a guard that ends up disabled. Extension-owned routines are now exempted with a warning, and the exposure stays visible as advisory CHECK 1x rather than vanishing - it reports pgtap (1079) on the preview. The sweep skips them too: pgcrypto-style helpers are called from column DEFAULTs and CHECK constraints, which are evaluated with the DML role's privileges. No extension owns a function in public on a clean database, so no deployed behaviour changes. **The repo was weaker than production.** find_user_by_email, get_session_status and update_api_key_last_used were locked down on the live project on 2026-09-08 but never captured as a migration, so a fresh deploy re-opened them. find_user_by_email is a user-enumeration oracle over auth.users and signup is open. The standing audit caught this itself on the preview - CHECK 2 named it - which is the drift that check exists for. Both remaining callers use SUPABASE_SERVICE_ROLE_KEY, verified on this branch, so service_role keeps EXECUTE and neither path changes. Also: the guard no longer hard-fails on a database without the Supabase roles (a plain restore), and the vault fixtures no longer assume the key is absent (U3) - both suites failed outright on any database that already had one. Retained-key assertions measure this run's delta instead of an absolute count. Suites on the preview: rotation 16/16, db access audit, and visibility 54/54 all clean; the standing audit reports no failing gated check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Exercise the anonymous read path, not just the grant The keep-list assertions only checked has_function_privilege for the seven allowlisted signatures. That proves the grant, not the path: an RLS policy expression is evaluated as the QUERYING role, so revoking a policy helper turns an anonymous SELECT into 'permission denied for function ...' rather than an empty result, and no privilege assertion can see it. The seeding is the part that matters. A policy expression runs PER ROW, so on an empty table it is never evaluated - and plugins and user_roles are both empty on a fresh database, which made the first version of this probe pass no matter what had been revoked. Caught by trying to make it fail. Sensitivity is stated from measurement, not assumption: with a row present, revoking can_view_plugin_row from anon does fail the probe and revoking authorize does not, because permissive policies are ORed and short-circuit. A helper in a policy that never has to be evaluated cannot be detected this way by any test, so the comment says so rather than implying broader coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the gated audit checks and the test generator provable Both halves of this are the same failure: a suite that is green while proving nothing. I hit that twice writing the previous commits - a recovery-code fixture over a secret_metadata row create_secret never wrote, and an anonymous-read probe over an empty table whose RLS policy therefore never evaluated - so these are not hypothetical. Finding 6. The four CI-GATED checks (1, 1b, 4, 5) only ever asserted HEALTHY, while every advisory check already had a fixture-based detection test. An inverted `not exists`, a wrong role literal or a typo in the evtenabled test would have left the gate permanently green. Each is now broken deliberately and asserted to report it: a routine granted to anon AFTER creation (which is how one gets past the event trigger), a revoked deliberate anonymous grant, a disabled event trigger, and a crypto routine exposed to authenticated. They have teeth by construction - the assertion is that the finding appears, so a broken audit query fails them. Finding 8. The generator gained --check, which regenerates in memory and compares without writing, and orphan removal for a generated suite whose .sql.in has been renamed or deleted - that file otherwise keeps being executed forever with stale content. Orphans are identified by a provenance banner rather than a hard-coded list, so a hand-written suite is never touched; verified that explicit_anon_and_org_visibility_test.sql is untouched. Each guard was proven by making it fire: a hand edit and a planted orphan are both reported by --check, and generate removes the orphan. CI now runs generate and --check as their own step, so a preparation failure is visible in the log instead of being attributed to the test run. Preview: rotation 16/16, db access audit 15/15, visibility 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Announce every project change, not only the plugin-initiated ones Panels could come up empty and stay empty, and the reason was that the startup restore told no one. ProjectChangeEvent was published from ProjectDataProviderImpl.selectProject, which is the path a PLUGIN takes. The two callers that matter most bypass it: WorkspaceApplier.applyWorkspace calls windowProjectState.selectProject directly when it restores the workspace, and BossTopBar's picker goes through selectProjectInWindow. Neither published anything. The ordering that produced the empty panel: DefaultPlugin is constructed in a DisposableEffect keyed on registries and window state, not on selectedProject, and it kicks off the plugin scan asynchronously while BossAppScaffold composes and the sidebar builds panels immediately. The restore meanwhile waits on WorkspaceManager's sequential Dispatchers.IO JSON reads. When those reads are slow the panel is built first, reads the "" that WindowProjectState seeds itself with, and renders its no-project state. The restore then lands, silently, and nothing remounts the cached component. Publishing from the state itself rather than from one caller catches all three. The bus is MutableSharedFlow(replay = 0), so a publish that never happens cannot be recovered by a later subscriber - which is why this could not stay per-caller. The publish is removed from selectProject rather than added alongside the collector; keeping both would double-fire on the plugin path. previousPath is seeded from the current value so the StateFlow's replay of it is not announced as a change. That seed is "" at startup, and telling every plugin the project just became "" moments before the real restore arrives is precisely the clear-yourself signal worth avoiding. One collector per window: projectDataProvider is `by lazy` on DefaultPlugin, which is per-window, so this cannot stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Publish from the state's own callback, not from a lazily-built provider The review's finding 1: observing `selectedProject` from `ProjectDataProviderImpl` put the fix behind two `by lazy` initializers. `projectDataProvider` and `applicationEventBus` are both lazy on `DefaultPlugin`, and the second is the only host path that creates the bus at all. So the collector started only if some plugin touched both before the workspace JSON reads finished - and "the reads are slow" is the premise of the bug. Touched after the restore, the collector seeds to the restored path and announces nothing; never touched, no caller is announced at all. The announcement now hangs off the `ProjectSelectionCallback` that `WindowProjectStateRegistry` installs when it builds the window's state, which `BossAppState` does eagerly in composition. `WindowProjectState.selectProject` is the sole mutator of the selection and invokes that callback synchronously, so every caller is covered with no coroutine, no scope and nothing lazy in the path. Being synchronous also makes `previousProjectPath` a faithful history rather than best-effort: there is no StateFlow conflation to collapse A -> B -> C into A -> C. The two registry entry points carried a copy of the wiring each; they now share one `newState`, so the announcement cannot be installed on some windows and not others. Finding 1's third mitigation, which is a hole of its own: `publishSystemEvent` was a no-op whenever the bus had not been created, so on a build where no installed plugin had touched `applicationEventBus` yet, no host event existed - not `ProjectChangeEvent`, not `AuthEvent`, not `TabEvent` - and with `replay = 0` none could be recovered. It now creates the bus instead of dropping. `getInstance` also re-checks the registry on every call rather than only at creation, so an instance that exists while the registry is empty can no longer strand the host's publisher. Findings 2 and 3: `ProjectDataProviderImpl` implements `DisposableProvider` and `DefaultPlugin.dispose()` releases it through a named delegate, alongside `logDataProviderDelegate` and `gitDataProviderDelegate`; its scope gets a `SupervisorJob`, matching `pluginScope`. Tests: `ProjectChangeAnnouncementTest` (6) pins a direct `selectProject` being announced, the previous-path chain, the seeding rule, the same-path suppression, both registry entry points, and the bus being created rather than dropping the event. Full `:composeApp:desktopTest` green: 3695 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 2: honest bus wording, atomic previousPath, three more tests Review (1/2) and (2/2). 1. `publishSystemEvent` does not save the event it creates the bus for. Correct - no registered publisher means nobody holds the bus, so it has no subscribers and that first event still reaches no one. The comment, the test name and the PR body all claimed otherwise. Reworded to what it actually buys: the host stops being permanently silent while it waits for a plugin to touch `applicationEventBus`. 3. `previousPath` was check-then-act on a plain field, and `ProjectDataServiceBridge.selectProject` is a suspend gRPC handler with no hop to Main (verified: `KernelBootstrap` registers it, the bridge calls `provider.selectProject` directly). Now an `AtomicReference.getAndSet`, so the read and the write are one operation. 5. The registry read-modify-write moved back inside `synchronized(this)`. Both fields are in fact `@Volatile` in boss-plugin-api 1.0.87 (checked the pinned jar), but the lock is uncontended after the first call and removes the question. The bus-before-publisher ordering is now written down, including why a publish landing in that window loses nothing. 6. `initialPath` KDoc no longer describes a production scenario it cannot reach; it is documented as defensive and test-reachable, and defaulted to "". 7. Three tests added: - a plugin-initiated selection is announced exactly once (catches both a silent regression to zero and an accidentally re-added publish); - the registry test now asserts the recent-projects half of the callback ran - deleting it from `newState` previously left every test green; - `dispose()` stops the recent-projects collector, which is the whole reason `projectDataProviderDelegate` became a named lazy. `Dispatchers.setMain(UnconfinedTestDispatcher())` for the class: the provider collects on `Dispatchers.Main`, which has no implementation in a plain test JVM, so its collector silently never ran. Unconfined also makes the dispose test deterministic rather than a sleep. Smaller: `SupervisorJob` comment describes it as future-proofing (there is one collector); `systemEventBusScope` is `by lazy`; the KERNEL-mode consequence of disposing the provider is documented on `dispose()`. Not taken: `replay = 1` for `ProjectChangeEvent`. It would close the mirror race, but it changes `events()` semantics for every subscriber and every event type on a bus whose consumers are out of tree. The body now says the race is narrowed rather than closed, and names the api-side `selectedProject` StateFlow as what closes it. `:composeApp:desktopTest` 3697 tests, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 3: unconditional announcement, ordered publish, drop dead register Review 3. 2. AGENTS.md: a bullet in the canonical "what a third-party plugin can observe" paragraph, since this widens *when* a filesystem path reaches every installed plugin. 3. The `expect` KDoc still said "a no-op if the bus has not been created yet". Only the `actual` had been corrected. Fixed, and it now states the part that IS still best-effort: replay = 0 means an unsubscribed event is gone either way. 4. The announcement sat downstream of `ProjectState.updateRecentProjects`, so a throw there skipped it AND left `previousPath` stale - every later selection reporting a previous path one step behind, silently, for the session. Now `try { recents } finally { announce }`: order preserved for anyone reading recents off the event, the announcement unconditional, the throw still propagating. 5. `getAndSet` closed the read-modify-write but not the publish order, so two concurrent selections could emit out of order. `previousPath` and the publish are now in one `synchronized` block (the AtomicReference is gone - the lock subsumes it). The KDoc says plainly what this still does not settle: `WindowProjectState` writes `_selectedProject.value` then calls the callback with no atomicity, so state and last event can still disagree. Closing that means locking upstream. 6. `ProjectState`'s async `loadRecentProjects()` assigns `_recentProjects.value` wholesale and could land mid-test, dropping a just-added path - only where `~/.boss/recent-projects.json` exists, so a developer-machine-only flake. A once-per-JVM settle in `@BeforeTest` puts that single file read before any test. The test paths stay deliberately non-existent, now with a comment saying why: the fire-and-forget saves are unordered, and `loadRecentProjects` reclaims entries whose directory is gone, so a real temp directory would leak where these self-heal. 7. `register` deleted. No production caller (`BossAppState:299` uses `getOrCreate`), and it overwrote an existing entry - handing back a state with a fresh announcer seeded to "" for a window that already had a project. `newState` stays; it is what keeps the wiring from drifting. 8. `@Volatile` dropped from `ApplicationEventBusImpl.instance` - every access is inside the lock now, and leaving it reads as if a fast path survives. Also: a test for the actual regression path, `applyWorkspace` announcing the project it restores, rather than a comment claiming to imitate it. Answering the grep you could not run: the only `projectChanges()` consumers in boss_plugins are the two fluck-agent panels (`FluckAgentViewModel`), and both assign `_bossProject.value` and re-sweep - idempotent on a repeat. Nothing used the repeat publish as a reload nudge. Recorded in the announcer comment. Item 1 was already in the PR body from round 2 ("The startup race is narrowed, not closed"), naming the mirror case and the api-side StateFlow that closes it. `:composeApp:desktopTest` 3698 tests / 368 classes, 0 failures. `detekt ktlintCheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 4: split the dispose out, stop tests writing the real ~/.boss Review 4. Items 1 and 3 were the before-merge ones. 1. Verified: ProjectDataServiceBridge.watchRecentProjects collects provider.recentProjects, a StateFlow, so cancelling the provider's scope on window close leaves that gRPC stream open and silently frozen for every out-of-process plugin. Taking the first option and SPLITTING the dispose out. Note this reverses what round 1 asked for, and the justification changed rather than the opinion: once the announcement moved to the registry callback, ProjectDataProviderImpl went back to being exactly what it was before this PR, so the leak is pre-existing and unrelated to the subject. Closing it properly means the bridge reading ProjectState directly instead of a per-window provider, which is its own change. The reasoning is on the class so it is not re-fixed by accident. 3. Verified and worse than described: MAX_RECENT_PROJECTS = 10, so test entries evict real ones from the developer's picker and no cleanup restores them. systemProperty("user.home", <build>/test-home) on the Test task. One run with it in place shows the existing suite was writing far more than recents to the real home - window-appearance-settings.json, keymap-settings.json, scrollbar-settings.json, recent-browser-pages.json, dashboard-stats.json and a ~/BossProjects/ directory all land in test-home now. The settle is gone with it: no recent-projects.json in test-home means the wholesale reload never happens. 2. publishSystemEvent now refuses the bus != null && systemPublisher == null state instead of falling through into a bus that may not be the registry's, with one warning rather than one per event. The comment at the fallback is true as written. 4. Both stale comments fixed (one construction path, not two). 5. hostProjectCallback(updateRecents, announcer) extracted so the try/finally claim is testable - ProjectState is an object, so a hard-coded call cannot be made to fail. 6. The publish-inside-the-lock trade is spelled out in the KDoc: what it buys (ordering), what it costs (an inline subscriber runs holding the lock inside selectProject), and that the contended path is untested. 7. The serial-execution dependency is recorded in the test class KDoc. Plus: multi-window isolation, and a test that makes the recents update throw. One thing this round caused and fixed: removing dispose() left two ProjectDataProviderImpl collectors alive on the test dispatcher. On a TestCoroutineScheduler a live coroutine is something every later runTest waits on, and the git classes timed out at 60s in full-suite runs while passing in isolation. Dispatchers.setMain(Dispatchers.Unconfined) rather than UnconfinedTestDispatcher(): same inline behaviour, not enrolled in a scheduler anyone else observes. It is also a real argument for the follow-up - without dispose() the provider cannot be constructed in a test without leaking. :composeApp:desktopTest 3699 tests / 368 classes, 0 failures. detekt ktlintCheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 5: reset test-home per run, stop getInstance stealing a publisher Review 5. 1. Confirmed reproducibly - build/test-home/.boss/recent-projects.json was sitting there from the previous run. doFirst now deleteRecursively() before mkdirs(), so the home is fresh per run rather than merely private. Without it the redirect only helped the first run and ProjectState.init's load reintroduced the same race from the second. Verified by running the full suite twice back to back. 2. Right, and sharper than it looks: the guard tested `bus` but wrote both fields, and `systemPublisher != null && bus == null` is exactly what ProjectChangeAnnouncementTest, BrowserAnalyticsEmissionTest and BossTabsComponentMoveTest install. Guard now tests both, so getInstance cannot take a publisher away from whoever set it. 3. Test added for the warn-and-drop branch, asserting the DROP rather than the warning - partialRegistryWarned is a one-shot process global, so "it warns" is unassertable after the first test to trip it. Uses a stub bus so the half-registry state is real. 4. The re-entrancy hazard is now documented as SAFE and why (synchronized is reentrant, previousPath advances before the publish), so it is not "fixed" later. The lock-widens-a-UI-hang-to-a-cross-thread-one point is in the same block. 5. Taken, including the optional ones: - hostProjectCallback keeps both failures (addSuppressed) instead of letting finally discard the first. Needs @Suppress("TooGenericExceptionCaught") - catching Throwable is the contract, and both are rethrown. - ProjectDataProviderImpl takes an injectable dispatcher (default Dispatchers.Main). This removes the global setMain AND the round-4 collector leak at the source rather than working around them, without reopening the DisposableProvider split. - Three more pins: a null window state announces nothing, applyWorkspace (restoreProject = false) announces nothing, unregister + getOrCreate restarts the chain at "". Not taken, with reasons: jvmArgumentProviders (the current form works, is config-cache-serialisable and passed CI on three OSes); widening the user.home redirect to plugin-path-utils (its test only mkdirs a directory every BOSS user already has). There is no CHANGELOG in this repo, so the release-notes line for the same-path change is an action item at release time, not a file change. Flagged in the body. Two consecutive full runs: 3703 tests / 368 classes, 0 failures both times. detekt ktlintCheck clean - it caught two TooGenericExceptionCaught and a long line in this round's own code first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address project announcement review feedback * fix(plugin-store): restrict plugin_downloads RLS to the server-side pipeline (#488) * Batch validation: promote latest reviewed dev changes to main (#463) * Fix #30: Resolve duplicate context menu label collisions * Cleanup: Remove dead code for #91 and #93 * chore: Sanitize SubmitResult.Error at construction rather than at one render site * Fix panel component lifecycle disposal * fixes * Guard panel lifecycle destruction failures * fix(logging): sanitize secret RPC failures and crash-report errors Three related gaps in what gets logged and shown when something fails: - SecretService.kt had no BossLogger in any of its ten catch blocks (#145). When the organisation migration broke all four secret RPCs (#144), the only WARN in the log came from the calling plugin - the code that actually failed said nothing. Every catch now logs via logger.warn(LogCategory.NETWORK, ...), reusing the already-sanitized failure for both the log and the returned Result rather than sanitizing twice. RoleService and RoleCreationService were checked and already log correctly - SecretService was the only silent one. - LogSanitizer.sanitizeExceptionMessage redacted a hostname only when it appeared inside a URL - a bare hostname (exactly what UnknownHostException.getMessage() produces, i.e. every DNS failure, and what a proxy-connect failure looks like) passed through untouched (#109). Added a narrow hostname pattern: lowercase-only labels ending in a short explicit TLD/`.internal`/`.local` list, with a negative lookahead so a package path that happens to end in a real TLD word mid-FQN (kotlinx.coroutines.internal.ScopeCoroutine, kotlinx.io.EOFException) is not mistaken for one - caught by the existing realistic-stack-trace test after the first version of the pattern redacted a live Kotlin package name out of it. - CrashReportService.SubmitResult.Error held a raw exception string, sanitized at exactly one render call site in CrashReportDialog (#110). The constructor is now private; SubmitResult.Error.of(...) is the only way to build one, and it sanitizes before the raw string can reach .message - a property of the type now, not something every future consumer (a copy button, a toast, a log line) has to remember on its own. Both construction sites (the service's two catch blocks, and the one in CrashReportDialog the issue names directly) now route through it. Tests: new coverage for the ten now-logging SecretService paths (via SupabaseWiringTest's existing sanitization-wiring guard, updated to recognize the "sanitize once into a local, reuse for log and return" shape SupabaseDataProviderImpl already used), six new LogSanitizerTest cases for the hostname fix (including the FQN/version-number false-positive guards), and a new CrashReportServiceTest for the sanitize-at-construction guarantee. * test(crash): extract construction tests from #404 Selected unchanged test file from Antriksh1984 original commit 99d2a5228023c541e825fe2714619c8228140369 (PR #404). Maintainer extraction only; subsequent API/scope adaptations are recorded separately. * chore(ipc): clarify advisory menu ID scope and fix formatting * chore(ui): finish obsolete toolbar comment cleanup * test(crash): adapt consolidated coverage and guard factory visibility Maintainer consolidation: use #311 companion invoke and a URL fixture instead of depending on #404 hostname redaction. Add private constructor/copy bytecode assertions. Preserve original contributor tests and implementation credit separately. * refactor(crash): consolidate construction sanitization in #311 Maintainer scope change: remove only the duplicate #110 implementation, retaining Antriksh1984 original #109/#145 work and commit history. Standalone construction tests are retained with attribution in #311; its Aditya8369 implementation supersedes Error.of. Keep render-time sanitization here until #311 merges and describe the remaining hostname coverage conservatively. * 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> * 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> * 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). reapChildr…
…isa-labs-inc#566) * Publish plugins with serialized payloads and environment credentials * fix(publishing): omit absent authors and verify script payloads in CI * test(publishing): require PowerShell for hosted fixtures * fix(publishing): preserve safe validation diagnostics * fix(publishing): explain invalid PowerShell store URLs * test(publishing): exercise Windows PowerShell 5.1 and 7 explicitly * fix(publishing): validate HTTP status and checksum on PowerShell 5.1 * fix(publishing): compute streamed hashes without module auto-loading * test(publishing): retain sanitized failure diagnostics in assertions * fix(publishing): decode binary JSON responses before parsing * fix: preserve publisher homepage and tag metadata on Windows --------- Co-authored-by: Raghav Bansal <273537979+Raghav-Bansal-15@users.noreply.github.com> Co-authored-by: Shivang <shivang.iitk@gmail.com>
|
Claude finished @kshivang's task in 4m 38s —— View job Review: PDF overwrite protection (
|
|
@dilatedtime The native re-presentation design is the right approach and the original state-machine coverage is thoughtful. Before merge, please make the disk probe fail closed ( |
|
@claude Please re-review exact head 6cc92bc. The bounded maintainer repair keeps the native overwrite-confirmation design, switches the production disk probe to fail closed via !Files.notExists, makes callback-to-extension policy testable and mandatory at both save call sites, and adds first-panel cancellation plus third-collision coverage with bounded fakes. Focused NativeFileDialogsTest, composeApp ktlint, and detekt pass locally under the shared build lock. The suggested custom second-panel title was intentionally not added: it is optional UX polish that would widen native-dialog plumbing, while the native panel/OS still owns exact-target overwrite confirmation. |
|
Claude finished @kshivang's task in 4m 32s —— View job Re-review: exact head
|
| Previous finding | State at 6cc92bc |
|---|---|
#1 Files::exists not fail-closed |
Fixed |
| #2 wiring unpinned, one-argument mutation | Fixed |
| #3 re-presented panel has no title | Declined - reasoning accepted, see below |
| #4 unbounded fakes, first-panel cancel, third iteration | Fixed (2 of 3 doc points remain open) |
#1 - the probe now fails closed, and the KDoc claim is now true
targetExists = { targetExistsOrUnknown(it, Files::notExists) } (NativeFileDialogs.kt:293) is the right inversion. Files.notExists returns false both when the file is present and when the answer is indeterminate, so an unstattable parent or a network volume that vanished while the panel was open now re-presents rather than waving the unconfirmed path through. The asymmetry lands the correct way: the cost of a false positive is one panel the user accepts, the cost of a false negative was their file.
Worth noting that this also retroactively makes the KDoc honest. NativeSaveTarget.kt:31 says a SecurityException escapes to answerOnce's fail-closed path - that is now the only exception class that can escape, which is exactly what the sentence claims. Under the previous Files::exists it was true by accident and misleading about IOException.
Extracting it as targetExistsOrUnknown(path, notExists) rather than inlining !Files.notExists(it) earns its keep: an indeterminate disk probe is treated as a collision asserts both directions of the inversion, so restoring Files::exists semantics fails a named test. That is the mutation gate the previous review asked for on this line.
#2 - the wiring is mandatory now, which is the part that matters
callbackType: Class<out BrowserCallback> has no default on showSave (NativeFileDialogs.kt:285), so both call sites must answer and a third one cannot be added silently. That is strictly stronger than what the previous review suggested: the old failure mode was deleting an optional named argument, and that mutation is now a compile error rather than a green suite.
requiredExtensionFor is pure, lives next to the state machine, and save-as-pdf callback requires the pdf extension pins both arms - the PDF one and the null for ordinary saves. The null arm is the one I would have expected to be skipped; pinning it is what stops a well-meaning "just always append the extension" change from adding a disk probe to every SaveFileCallback.
I also checked the behaviour was not lost in the move: the old ?.let { pathWithExtension(it, PDF) } is now inside chooseSaveTarget, which returns target (the appended path) rather than selected on the accepting branch. The extension still reaches Chromium.
#3 - declining the second-panel title is defensible
I accept the reasoning. The OS sheet on the second Save says "report.pdf" already exists. Do you want to replace it?, which is the explanation - it arrives one beat later than a title would, and it names the real target, which is the property that actually had to hold. Weighed against threading a title through pick and newDialog for every save panel, deferring is a reasonable call.
The one thing I would still do is write the omission down. This repo's conventions are emphatic about recording what was seen and deliberately not done, and a silent re-presentation is exactly the kind of thing a future reader files as an oversight. One sentence in the chooseSaveTarget KDoc saying the second panel is deliberately untitled because the OS replace sheet names the target would settle it permanently.
#4 - coverage
removeFirstOrNull() plus assertTrue(picks.isEmpty(), …) in all three multi-pick tests is the right shape: an extra-iteration regression now fails as a named drain assertion instead of a NoSuchElementException from a mutable list. renaming to another colliding target can reach a third panel is better than the bare third iteration I suggested, because it exercises the rename path - the user changing their mind at the second panel, not just re-confirming.
cancelling the first required-extension panel refuses the save asserting probes == 0 is a nice touch. It pins that cancellation short-circuits before any filesystem work, not merely that the result is null, so a refactor that hoisted the probe above the null check would fail here.
Two of the three documentation points from that item are still open, both cheap and both non-blocking:
- The loop's termination contract is not written down. It is bounded only because
pickis a modal panel a human answers. A non-interactivepickthat returns a cached colliding choice spins forever.renaming to another colliding target…demonstrates the loop is user-driven but does not state the requirement, and the KDoc's "the native panel needs a display" is about testability, not boundedness. - The extension-less collision is still unremarked. Typing
reportwhere a file literally namedreportexists makes the first panel ask "replace report?" while BOSS writesreport.pdfand leavesreportuntouched. Pre-existing, not worsened here, and now the only confusing case left in this area - which is precisely why it wants a sentence.
What I would still change (non-blocking)
The panel's filter and the appended extension are two copies of one fact. onSaveAsPdf hand-writes extensions = listOf(PDF) at NativeFileDialogs.kt:179, where PDF is a file-private const, while the appended extension comes from requiredExtensionFor returning the literal "pdf" in a different file. They must agree - the filter narrows the panel to .pdf, the policy appends .pdf - and nothing enforces it. showSave already receives callbackType; deriving the filter from the same lookup would delete the duplicated literal and extend the existing save-as-pdf callback requires the pdf extension test to cover both halves. Small, and it closes the last unpinned join in a change whose whole point was pinning that join. Fix this →
The resolved/result flag pair still has no note. Correct, but it reads harder than early returns, and without a comment saying why (detekt's LoopWithTooManyJumpStatements, presumably) somebody will "simplify" it back and re-introduce the jumps.
Minor: a SecurityException from the probe now reaches answerOnce's catch (t: Throwable), which cancels and rethrows - so it produces the warn log plus an uncaught-exception trace on the EDT. Correct (the cancel is what matters) and effectively unreachable without a SecurityManager, just noted since this change created the first path by which one can arise here.
The one thing only a mac can answer
The design now shows two native panels back to back, synchronously, inside a single invokeLater body. showModal disposes the first FileDialog before newDialog builds the second, and ownedFileDialog resolves its owner live from KeyboardFocusManager.getCurrentKeyboardFocusManager().activeWindow. Because the loop has not yet returned to the event pump, the activation events restoring focus to the Compose window may not have been dispatched - so the second panel could be built with a null owner, or owned by the just-disposed dialog. That is the exact condition ownedFileDialog's own KDoc exists to prevent, and the symptom would be a panel the user did not ask for opening behind the window.
isAlwaysOnTop = true should mask the worst of it, and this is speculative rather than a finding - I cannot run AWT against NSSavePanel from here. But it maps precisely onto the unchecked "macOS visual interaction tested manually" box, and it is the only behaviour in this change that unit tests structurally cannot reach. If one manual check is done before merge, make it this one: save as PDF with the extension stripped onto an existing report.pdf, and confirm the second panel comes up in front and owned.
Branch integration
Resolved as far as the merge is concerned. git diff origin/dev HEAD is the three files and nothing else, matching GitHub's +261 / -6. The branch still carries 23 commits in its history, but they contribute no content over dev after 0cae0e7, and a squash merge makes the point moot either way.
Summary: the fail-closed probe and the mandatory, test-pinned callback-to-extension policy both land correctly, the new tests fail on the mutations they exist to catch, and the diff is clean against dev. Declining the second-panel title is a reasonable trade given the OS sheet names the real target. Nothing blocking from me - I would add the two KDoc sentences (loop boundedness, the deliberate absence of a title) and fold the duplicated "pdf" into one lookup, and get the macOS back-to-back-panel check done by hand.
I did not run Gradle: a cold :composeApp:desktopTest here needs a JxBrowser license and a long build, and per AGENTS.md the app is yours to run. The review above is from reading the code, and your report of the focused suite, ktlint and detekt passing is consistent with what I see - no detekt baseline entries exist for these files, NativeSaveTarget.kt ends with a newline, and the new prose carries no U+2014.
• branch fix/native-save-extension-overwrite
* 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…
Pull Request
Description
Prevent macOS Save as PDF from silently replacing a different file after BOSS appends the required
.pdfextension.Previously, entering
reportmade the native panel confirmreport, then BOSS returnedreport.pdfto Chromium. Ifreport.pdfalready existed, Chromium could replace it even though the user was never asked about that actual target.This change re-presents the extension-appended target in the native Save panel only when it already exists. The OS then owns confirmation for the exact path that will be written. Cancelling the second panel cancels the browser callback.
Type of Change
Version Impact
Testing Checklist
Local Testing
NativeFileDialogsTestsuite passes on the rebased head:composeApp:detektpasses:composeApp:ktlintCheckpassesThe full suite ran 5,277 tests: 5,275 passed. The only two failures are unrelated existing symlink tests (
DesktopFileScannerTestandDiscardDownloadContainmentTest) that fail while creating a link with Windows error "A required privilege is not held by the client". Other symlink tests in the suite skip for the same environment limitation.Platform Testing
Code Quality
devBefore / After
Before: Save
report; the OS checksreport; BOSS writesreport.pdf, which may already exist and was never confirmed.After: If
report.pdfexists, BOSS reopens the native panel prefilled withreport.pdf. Save invokes the OS overwrite prompt for the real target; Cancel returns no path.Implementation Details
showSaveaccepts an optional required extension used only by Save as PDF.chooseSaveTargetloops only while extension appending changes the path to another existing target.answerOncecancellation guard.Review Focus Areas
Pre-merge Checklist
devat submission timeReview sweep integration (2026-09-16)
Contributor credit: @dilatedtime (Aryan Raj) authored the complete PDF target-selection fix and five regression tests in
395c7a5d. Maintainer review commit0cae0e7a0only integrates currentdev, retaining its unrelated filesystem implementations. The final three-file diff against that dev base is byte-identical to Aryan's original commit; no original implementation credit is claimed for integration.Review validation: nine direct production-policy scenarios pass, including renamed collisions and cancellation. Focused Gradle tests and module lint/detekt are queued behind the shared build lock; exact-head hosted CI is running after normal fork-run approval. Native macOS visual interaction has not been performed in this sweep. Claude review was requested once and is pending.