Repository navigation
fix(kernel): require verified caller identity for the run-configuration bridge - #634
Conversation
|
@claude Please review current head |
|
Claude finished @kshivang's task in 5m 12s —— View job Review of
|
|
@Antriksh1984 Thank you for the concrete IPC security work. Current repair head: Your host-held configuration lookup closes the untrusted command payload path and retains all six RPC identity gates. Reviewer commit adds unary and mid-stream revocation regressions, logs stream refusals, records execution requests using caller plus provider-held configuration id, records scan state on lookup misses, and corrects the KDoc to say auto-detected configurations only. Operator-saved configurations are not in this provider. The real remaining behavior risk is a Run click across a rescan: scanning empties the provider list and generates new UUIDs, while RunConfigDataProviderProxy.execute swallows the refusal. I did not add the suggested request-field tuple fallback: it does not solve the empty-list interval, and selecting a replacement execution from caller-supplied fields needs an explicit stale/project identity contract. The current exact-id allowlist therefore remains fail-closed. Likewise, changing PERMISSION_DENIED to NOT_FOUND alone does not repair a client that swallows both; retain the existing allowlist refusal contract until client behavior is coordinated. The new host log identifies whether a miss happened during scanning without logging arbitrary request payloads. Next steps: make stale/unknown configuration refusal visible to the run client (or establish a safe project-scoped refresh/retry contract), and add a test for Execute during/after rescan proving that no caller-supplied command executes and that the user receives a useful result. Cover successful known-config forwarding, unknown ids, and both new revocation tests. The proxy/runtime follow-up is cross-repository context, not taken over by this assignment. The latest requested Claude review is #634 (comment). Shared authentication-helper consolidation and stale transport inventory comments remain #575 context; no cross-group PR was changed. Avoid introducing another generic flow abstraction in this bounded security patch: the existing synchronous capture sites remain explicit, and refusal logging is centralized locally. Original-head CI and actual XML were verified green on Linux, macOS and Windows. That evidence does not validate these new reviewer changes. Updated-head CI is queued/in progress; local Gradle validation could not acquire the shared build lock during this bounded review. This is an infrastructure/validation handoff, not evidence of an original contributor CI failure. All repairs are reviewer work and are not counted toward your original contribution score. No app instance or live database was launched or stopped. Per the assigned bounded-disposition policy, I am converting this PR to draft after this handoff. No checks were bypassed and no unvalidated merge was attempted. |
|
Confirmed current head |
|
@claude Please review current head |
9f29c91 to
475b88c
Compare
|
Claude finished @kshivang's task in 5m 27s —— View job Review of
|
| Repair | Where |
|---|---|
| unary + mid-stream revocation regressions | RunConfigServiceBridgeTest.kt:179, :188 |
| stream refusals logged | RunConfigServiceBridge.kt:152-159 (refuseIdentity) |
| attributed execute audit line | :106-110, caller + provider-held config.id |
| scan state recorded on a lookup miss | :135 (isScanning) |
| KDoc corrected to auto-detected only | class KDoc, "operator-saved configurations are not exposed by this provider" |
What I re-verified holds
All six RPCs are gated, and the stream capture is correct. run_config.proto declares six; all six refuse without an identity. CURRENT_IDENTITY.get() is read in the RPC method body before the flow { } is collected, and the closure holds the supplier, which is what makes the per-emission re-check possible. The check runs before emit, so no value escapes after revocation. ?: refuseIdentity(...) returning Nothing is what smart-casts currentIdentity non-null for the emission check - so unlike DownloadServiceBridge.kt:47's currentIdentity?.invoke(), the null branch here is provably unreachable rather than defensively re-tested. Correct, and slightly tighter than the sibling.
Fail-closed is complete and not an outage risk. No interceptor → null identity → every RPC refuses; the bridge is registered only on the one server built with a real registry (KernelBootstrap.kt:987).
The allowlist and the client's list are the same StateFlow, which is the property that keeps them consistent. I chased this further than last time because execute now depends on a mirror it previously never read. RunConfigurationDataProviderImpl.kt:37-41 mirrors the global RunConfigurationManager on Dispatchers.Main; watchDetectedConfigurations serves that same mirror. So the client can never hold an id the allowlist has not held - and if that mirror collector ever stalled, execute would refuse everything and the client's list would be empty too, which is at least self-consistent rather than a phantom-id state. The provider is by lazy per DefaultPlugin (per window), but RunConfigurationManager is a singleton, so multiple windows do not divide the allowlist.
Revocation is defence-in-depth, not a live path. revoke is only called from ProcessSpawner.kt:105-107 (child exit) and KernelBootstrap.kt:184 (reap), so a running client's token is not revoked out from under it. The tests are still the right tests; nobody should "optimise" the per-emission check away on the grounds that it never fires in production.
Findings
1. [correctness] A blank requested id can match, and the sibling bridge guards exactly this.
RunConfigServiceBridge.kt:130 is find { it.id == requestedId } with no blankness guard on either side. ExecuteConfigRequest.configuration is an unset message for a caller that sends ExecuteConfigRequest.newBuilder().build(), so request.configuration.id is "" - and that matches any provider entry whose id is blank. Nothing in today's detector produces one (DesktopMainFunctionDetector mints UUIDs), so this is not exploitable at this head; it is an allowlist that silently degrades if any future provider, adapter or hand-built RunConfigurationData ever carries a blank id, and the caller does not even have to guess anything to hit it. DownloadServiceBridge.kt:139-148 guards precisely this shape on both sides (requestedPath.takeIf { it.isNotBlank() } and item.destinationPath.isNotBlank()), which is the in-repo precedent this bridge is otherwise following line for line. Two lines, same reasoning, closes it permanently.
Fix this →
2. [answer] PERMISSION_DENIED for an unknown id: keeping it is the consistent call, but the caveat should be written where the refusal is.
You asked me to recheck this. My previous review argued for NOT_FOUND; having now read the sibling that landed on dev, I think keeping PERMISSION_DENIED is the better choice here, because DownloadServiceBridge.kt:154 already returns exactly that for the same class of allowlist miss ("this path is not a tracked download"). Two bridges answering the same question two ways is worse than one arguably-imprecise code, and changing it should change the family together.
What the sibling also does, and this one does not, is document the race at the refusal site: DownloadServiceBridge.kt:130-132 says "The provider snapshot is sampled for UI delivery; a newly created download remains refused until that snapshot includes it. Callers should use paths observed from watchDownloads." That sentence is exactly the rescan story here - DesktopRunConfigurationManager.scanProject sets _detectedConfigurations.value = emptyList() for the duration of the scan and the detector mints fresh UUIDs after it - and knownConfigurationOrRefuse's KDoc (:121-125) does not say it. One sentence, and the two bridges tell the same story to the next reader.
Fix this →
3. [answer] The client swallow is in this repo, so only half of the follow-up is cross-repository.
Your handoff called the proxy/runtime follow-up cross-repo context. The file that eats the refusal is here: plugin-platform/plugin-api-ipc/.../RunConfigDataProviderProxy.kt:129 (catch (_: Exception) {} on execute, and the same at :113 scanProject and :136 clearError). Nothing in this repo constructs RunConfigDataProviderProxy - I grepped; the class is a published library the out-of-repo runtime wires - but the swallow itself is repairable here without touching the other repo. What genuinely needs cross-repo coordination is only the plugin-facing contract, since RunConfigurationDataProvider.execute returns Unit and throwing from it changes the pinned API's behaviour.
Two concrete details for that follow-up:
- The three watch loops (
:44-99) treat every status as transient and reconnect with 1s→30s backoff forever. For a refusal that will never become allowed, that is a permanent loop - benign today only because revocation coincides with child teardown (see above), so the loop dies with the process. Anyone hardening this should treatPERMISSION_DENIED/UNAUTHENTICATEDas terminal rather than retryable. - The cheapest visible-failure repair that needs no API change is to set
_lastErroron a refusedexecute, since the run panel already renderslastError. Caveat worth knowing before doing it:_lastErroris also driven bywatchLastError, so a host-side change would clobber a locally written message - which is fine for the revoked case (the stream is refused too, so nothing overwrites) and lossy for the stale-id case.
4. [coverage] Neither scanProject nor execute has its forwarded arguments pinned.
FakeRunConfigurationDataProvider records only the method name for scanProject (:275), and executed captures the config but never the windowId. So the suite would stay green if scanProject forwarded a wrong path, dropped windowId, or execute sent the wrong window - and windowId is caller-chosen on both RPCs. Recording projectPath/windowId in the fake and asserting them is a few lines and closes the one part of the request this bridge does still pass through untouched.
Fix this →
5. [coverage] The rescan regression you asked for next is mechanically covered but behaviourally undocumented.
"Execute during/after a rescan" and "unknown id" reach the same branch through the same predicate, so RunConfigServiceBridgeTest.kt:159 already proves no caller-supplied command runs in that window - the security half of your next step is done. What is missing is the behavioural assertion: observe a config, empty the list as a scan does, execute its id, assert the refusal and that nothing ran. Worth adding as the record of an accepted behaviour rather than as new coverage of a new mechanism, and it will pin the contract if anyone later adds a tuple fallback.
6. [coverage] watchIsScanning and watchLastError have no success assertion on their values.
The revocation test proves a first emission arrives on watchIsScanning but asserts nothing about it, and watchLastError is only ever refused. That leaves error ?: "" (:91) - the only non-trivial mapping in the three watches, and a lossy one, since a client cannot distinguish "no error" from "an empty error string" - untested in both directions.
Non-blocking notes
- Refusal log wording differs from the other five bridges.
:155says "no current verified process identity on this call"; Secret, Role, Supabase, Download and PluginUI all say "no verified process identity on this call". This bridge is the only one that logs stream refusals at all, which is an improvement worth keeping - but aligning the string means an operator can grep one phrase across all six. - Sixth private copy of
authenticatedCallerOrRefuse+NO_IDENTITY. Noted only; you explicitly ruled out an abstraction in this bounded patch and I agree with that call for this PR. The five on dev are already identical, so this is a family-wide cleanup or nothing. - Two shared-module KDocs now understate the gated set by half.
ProcessIdentityInterceptor.kt:21-24still says "the three that currently make it" andBossIpcServer.kt:33-40names only Secret and RoleManagement in what is an operational warning ("Any kernel server hosting one of those bridges must be constructed with a real registry"). The real count is six: Download, PluginUI, RoleManagement, RunConfig, Secret, Supabase. This drift predates this PR (Download and Supabase landed without updating it) and fixing it editsmodules/boss-ipc, which widens this PR's blast radius - reasonable to defer, but it is the stale doc most likely to mislead whoever wires the next server. scanProject's path and both RPCs'windowIdremain unconfined (pre-existing, unchanged here). An authenticated caller can still scan any absolute path, read the resulting absolute file paths and generated commands back offWatchDetectedConfigurations, and clobber the operator's detected list - and directing a known configuration's execution at an arbitrarywindowIdis still unvalidated. Identity gating does not address either; Remote UI: StreamUI has no plugin identity check — one plugin can bind another's surface #53 follow-up territory, not this PR's.provider.calls.isEmpty()at:109is still vacuous for the three watch RPCs (the fake cannot record a property read). The stream refusals keep their teeth viaassertFailsWithontake(1), so this is cosmetic.
· branchfix/run-config-caller-identity
|
@Antriksh1984 Your provider-held exact-id execution closes the untrusted command payload path, and all six RPC gates and prior revocation repairs survived the clean dev replay. I verified your update: the prior repair head did pass all ten checks and six focused tests on every supported OS. That earlier validation blocker was resolved, not an author failure. Current head Remaining substantive integration issue: DesktopRunConfigurationManager clears the detected list and remints UUIDs on rescan; plugin-platform/plugin-api-ipc/RunConfigDataProviderProxy.execute catches every exception and returns Unit. A stale Run click therefore silently does nothing. The proxy source is in this repository; only its published runtime consumption is cross-repository. Please expose a durable client-visible stale/unknown execution error (avoid watchLastError immediately overwriting it), or define an explicit safe project-scoped refresh/retry contract. Add proxy/UI integration tests for during-scan and post-scan stale clicks, no caller-command execution, visible failure, recovery on selecting a fresh id, and cancellation propagation. Do not silently resolve a replacement from caller-controlled command/path fields. Shared transport/helper inventory remains #575 context. Extra watch value/subscription assertions are follow-up test strength, not a claim that the current gates leak. No runtime release, native UI exercise or live database operation was attempted. I am converting this PR to draft for the concrete client behavior above and current-head validation; pending CI is not an original contributor defect. Original score remains 8.5/10 and excludes reviewer repairs. |
|
Final validation update: |
* 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 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 buil…
…a-labs-inc#635) * fix(kernel): require verified caller identity for the Git bridge GitServiceBridge had no caller-identity check on any of its fourteen RPCs. Any process able to open a connection to the kernel IPC server - not only the plugins the host itself loaded - could read the open project's full commit history and working-tree status, and could discardChanges (destroying uncommitted work), checkout, cherryPick or revert against the project's real git repository with no credential at all. Gate every RPC behind authenticatedCallerOrRefuse(), the same PERMISSION_DENIED-on-no-identity pattern PluginUIServiceBridge established for BossConsole#53. The four watch streams (file status, commit log, is-repo, is-loading) capture identity synchronously before returning and re-check it on every emission, so a revoked token stops a live stream rather than only new calls - matching the shape PR risa-labs-inc#550's review settled on for this bridge's siblings. * style(tests): wrap chained builder calls in GitServiceBridgeTest Real CI's ktlintDesktopTestSourceSetCheck flagged chain-method-continuation violations (a local run had missed this) - wrap the affected builder chains onto their own lines, per ktlintFormat. No behavior change. * test(kernel): cover revoked callers and log stream refusals --------- Co-authored-by: Shivang <shivang.iitk@gmail.com>
…a-labs-inc#645) * fix(kernel): require verified caller identity for the Log bridge LogServiceBridge had no caller-identity check on any of its nine RPCs. Any process able to open a connection to the kernel IPC server - not only the plugins the host itself loaded - could watchLogs to see the live stdout/stderr stream of the running BOSS process, or call exportLogs to pull its full history, with no credential at all. Unlike a bridge whose values are deliberately routed through LogSanitizer at the call site, nothing about this stream is scrubbed for IPC exposure - it is whatever every logger.info/warn/error call site across this large and growing codebase happened to write, which is not a guarantee that none of it is a stack trace, a file path, or something else nobody meant to expose to an arbitrary local process. Gate every RPC behind authenticatedCallerOrRefuse(), the same PERMISSION_DENIED-on-no-identity pattern PluginUIServiceBridge established for BossConsole#53. The four watch streams capture identity synchronously before returning and re-check it on every emission, matching the shape PR risa-labs-inc#550's review settled on for this bridge's siblings. * test(kernel): verify revocation and diagnose stream refusals --------- Co-authored-by: Shivang <shivang.iitk@gmail.com>
…abs-inc#647) * fix(security): randomize the IV for encrypt_text/decrypt_text encrypt_text called the 3-arg extensions.encrypt(plaintext, key, 'aes'), which pgcrypto documents as using an all-zero IV, so encryption was a pure function of (plaintext, key) alone: two equal passwords produced byte-identical ciphertext. Because create_secret encrypts server-side and RLS lets a user read their own password_encrypted back, any signed-in user was an encryption oracle - grouping a dump by ciphertext names every set of users sharing a password, with no key required. The same defect applied to recovery_codes_encrypted and twofa_secret. encrypt_text now draws a fresh random 16-byte IV per call and stores 'v2:' || base64(iv || ciphertext); decrypt_text dispatches on that prefix and falls back to the original zero-IV path for anything not yet migrated, so this deploys without a flag day. A backfill converts existing rows in all three columns, guarded by NOT LIKE 'v2:%' so a partial or repeated run is a no-op. Deliberately scoped to this one defect (BossConsole#618 names three). Defect 2 (the encryption key truncated to 32 string characters instead of 32 bytes) is not touched here: ops/rotate_master_encryption_key.sql records that a deployed key may be hex or base64, and this migration cannot tell which is live in any given project's Vault - guessing wrong would make get_encryption_key()'s result unusable and break encryption for every row on deploy. Defect 3 (no MAC/AEAD) needs its own wire-format review. ops/rotate_master_encryption_key.sql itself will need a follow-up before it can run again, since it manipulates raw ciphertext bytes assuming the old no-IV, no-prefix layout - both gaps are documented in the migration's own header. This could not be run against a real Postgres/pgTAP instance in this environment (no supabase/docker CLI available here) - the added test was written and reviewed by hand, and hosted CI's Database (pgTAP) check is the actual verification gate. * fix(security): un-qualify substring(... from ...) so it parses CI's Database (pgTAP) check caught what local review couldn't (no Postgres available in this sandbox): pg_catalog.substring(x from y) is a syntax error. substring's SQL-standard FROM/FOR trailing syntax is recognized by the parser only on the bare function name - schema-qualifying it makes PostgreSQL treat it as an ordinary call, where FROM is not a valid argument separator. 20260909000000_encrypt_totp.sql's own decrypt_text already calls substring unqualified for exactly this reason; this migration's three call sites (envelope/iv/body extraction in decrypt_text, and the twofa_secret backfill) now match that convention. Both functions' search_path already resolves the unqualified name to pg_catalog's substring, so nothing else about name resolution changes. * fix(security): make key rotation understand the v2 IV envelope CI's Database (pgTAP) check caught what local review couldn't (no Postgres available in this sandbox): the pre-existing master_key_rotation_test.sql runs ops/rotate_master_encryption_key.sql against real fixture data, and that script's re-encrypt loop calls extensions.decrypt/encrypt directly on raw ciphertext bytes, assuming the old zero-IV, no-prefix layout. The 'v2:' prefix the previous commit's encrypt_text now writes is not valid base64, so decoding it raised - exactly the gap this migration's own header comment already named as a follow-up, just sooner than expected. Added a pg_temp helper, rotate_ciphertext, that re-encrypts a mapped column's value under the new key while preserving whichever envelope it already carries: a 'v2:' row is decrypted with decrypt_iv under the old key and re-encrypted with encrypt_iv under the new key AND a fresh IV (a function rather than an inline SQL expression, specifically so gen_random_bytes(16) is evaluated once and reused for both the encrypt_iv call and the stored prefix - two calls would produce two different values and corrupt the row). A legacy row - every column before this pair of migrations, and still qbo_token_state / google_token_state going forward, since those are untouched by the IV migration - stays zero-IV, matching what it already was. Rotation's contract stays "same shape, new key"; upgrading a row's shape is the encryption migration's job, not rotation's. Updated the IV migration's own header comment, which had flagged this as a still-open follow-up, to point at the fix now landing alongside it instead. * fix(security): preserve plaintext bytes during IV upgrade * fix(security): make IV backfill atomic and diagnose unreadable rows --------- Co-authored-by: Shivang <shivang.iitk@gmail.com>
31f41a9 to
dac77a0
Compare
|
Rebuilt this PR's branch cleanly onto current dev (the prior head had accumulated unrelated drift - GitServiceBridge/LogServiceBridge, the now-merged encryption migration, etc.) and closed the remaining "client-visible stale-Run" item from the last review:
Added Current head |
|
@claude Please review current head dac77a0 against current dev, including contributor updates since the previous handoff. Verify retained identity/revocation protections, regressions and tests. Specifically check the new proxy lastError behavior under real watchLastError emissions, cancellation and non-stale RPC failures. |
|
Claude finished @kshivang's task in 5m 23s —— View job Review of
|
…nc#606) (risa-labs-inc#648) Global Search matched an open tab only against its title, and TabResult hardcoded url = null / filePath = null on every hit. A browser tab whose title never mentions its domain, or an editor tab someone wants to find by path rather than filename, could not be found at all. searchTabs now also scores a browser tab's FluckTabInfo.currentUrl and an editor tab's EditorTabInfo.filePath, through the same substring- containment gate (proseScore) searchRecentPages already uses for a page's URL, since a fuzzy subsequence match over a whole URL or path is close to meaningless. TabResult carries the real url/filePath instead of null, and the tab row's subtitle now leads with the workspace name (disambiguating identical URLs open in different workspaces, or across windows) followed by the url/filePath/tabType, in place of the old "tabType • workspaceName" line. 11 tests cover: URL-only and file-path-only matches, both fields populated for the right tab type and left null for the other/a blank path, a short query not matching a URL by fuzzy accident, title+URL both matching taking the higher (title) score with title highlight ranges preserved, live navigation changing which URL matches (and the old URL no longer matching), directory-path matching without a subsequence false-positive, and no-open-tabs contributing nothing rather than failing.
* 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 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 buil…
|
@Antriksh1984 Your original six identity gates/provider-held execution design and your new proxy error/cancellation work are useful and retained. I rechecked your update rather than repeating the old stale-Run handoff. All ten checks on your Current Claude review confirmed the security boundary and identified two concrete client defects: a locally written error could not be dismissed when host null-to-null StateFlow conflated, and every RPC failure was described as an unknown configuration. Reviewer repair Claude reconciliation: findings 1-6 addressed by the error-state repair, removal of the inaccurate overwrite comment, an emitting fake stream and new regressions. Optional additional bridge watch-value/property-access assertions remain test-strengthening, not a missing identity guard. Its suggestion to infer stale ids from PERMISSION_DENIED is rejected: that same code also means missing identity; generic wording is accurate for both. Its same-principal token-reissue observation is incorrect for the actual registry: CURRENT_IDENTITY captures the original token; issue invalidates the previous token, so the old stream resolves null, not the new token. Shared documentation/helper/runtime retry work stays #575; current dev has #651's integration fixes and that PR is confirmed merged. Remaining gate is validation on the repaired head, not a demonstrated author/source defect. Diff whitespace/newlines pass. My first local command used an incorrect Gradle project path (reviewer error); the corrected command waited on the shared lock without starting Gradle and I cancelled only that queued command to keep this bounded. No local pass is claimed for the seven new tests, and prior-head green CI is not evidence for them. Next steps: run Converting to draft for this validation-only handoff under the requested bounded disposition policy. |
|
@Antriksh1984 Your six verified-identity gates, provider-held exact-id execution, and client-visible failure work are retained. I verified your repaired replay and its ten green checks rather than repeating the prior handoff. Original contribution rating remains 8.5/10; the additional repairs below are agent work. Current dev/main were identical at 5a2e818. Your older ancestry conflicted with the promoted baseline in two documentation sections. Integration commit 940cf69 preserves current dev in both and leaves only the four intended RunConfig files in the net diff; your original branch history is retained by ordinary fast-forward pushes. The single new Claude review confirmed the security boundary and found a valid follow-up: old local run feedback could mask a later failed scan. Repair 8cd4fe1 now lets a changed nonempty host failure supersede local feedback, and beginning a scan clears local run feedback. Unlike the suggested unconditional clearing on every nonempty emission, it does not treat a replay of the same host snapshot as necessarily newer. Generic text is now only “Run could not be started.” Cancellation tests observe that execution never continues after the cancelled Scan/Clear call (and join it); transport/refusal messages are compared directly; three new tests cover newer scan failure, successful run preserving host error, and scan clearing local feedback. Added caller-only scan audit, without logging caller-chosen paths. Claude findings 1–6 are addressed by these changes. Finding 7 is already resolved in current dev: the interceptor no longer maintains a stale enumerated bridge list. Additional stream value/property-access tests and distinct revocation log stages remain optional coverage/diagnostic improvements; no missing guard is asserted. No repeated review request, and no independent Claude approval of the final repair is claimed. Final local validation on repaired content PASS: 8 RunConfigServiceBridgeTest + 12 RunConfigDataProviderProxyTest, zero failures/errors/skips; composeApp and plugin-api-ipc ktlintCheck and detekt all pass, using the required with-build-lock.py. Intermediate failures were my test formatting, a mistyped test selector, and a test returning a non-Unit value; all corrected, not author defects. Logs and XML are retained in this sweep's artifacts. Remaining gate is fresh hosted CI on 8cd4fe1, not a demonstrated remaining source defect. Converting to draft for a bounded validation-only handoff. Next steps: inspect the final head’s platform/build-summary, quality, compatibility and database checks; when green, refresh dev and verify the net diff/mergeability, restore ready and perform the authorized exact-head squash into dev under with-dev-merge-lock.py. If dev advances into the affected code/build path, revalidate the integration first. Do not substitute earlier-head green checks. Contributor credit remains explicit in the PR description. #653 Windows run disambiguation and #575 shared transport/helper work are distinct read-only context, not duplicates. No native visual exercise, external runtime publication, app launch/stop or live database deployment. Existing plugin error-banner/dismiss wiring was source-inspected; no layout changed. |
|
Confirmed current head `8cd4fe14` (scan-error precedence by recency + strengthened proxy regressions) - all ten hosted checks green, and locally: `RunConfigServiceBridgeTest` + `RunConfigDataProviderProxyTest` pass, ktlint/detekt clean on both `composeApp` and `plugin-api-ipc`. Restoring ready-for-review. |
|
@Antriksh1984 Rechecked your ready-for-review update against exact head The latest Claude review (5663110604) remains applicable and its findings 1–6 are repaired: changed host error precedence, scan-start dismissal, neutral failure text, non-continuation assertions for cancellation, direct transport/refusal comparison, host-error retention and caller-only scan audit. Finding 7’s stale inventory is already absent in current dev. A repeated nonempty host snapshot is not necessarily newer, so the repair correctly avoids clearing newer local feedback merely on replay. Optional stream-value/property-read tests and distinct revocation diagnostics are not missing authorization gates. No unresolved review threads or demonstrated substantive defect remain; no new Claude request is necessary for this already reviewed/repaired head. Six authenticated RPCs and exact nonblank provider-held execution satisfy the useful host security intent. Plugin error-banner/dismiss wiring remains compatible. Current dev’s intervening #650 changes are browser swipe navigation, not the bridge/adapter/build paths. #653 Windows disambiguation, #575 transport work and #666/#672 other identity bridges remain distinct read-only context; no duplicate is being closed. Proceeding with the authorized exact-head squash into dev under the shared merge lock, subject to fresh state/check/mergeability preflight. Your original contribution and later fixes remain explicitly credited; original score stays 8.5/10, excluding prior agent repairs. No native visual or external-runtime end-to-end exercise is claimed; no layout changed. |
…on 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 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 sw…
…on 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 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 sw…
…695) * 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 registr…
…dow closes (#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…
…dow closes (#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…
* 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…
…ive paths to the walk (#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 (#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…
…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 (#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…
* 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…
#564) * Constrain workspace persistence to validated local IDs * test(workspace): preserve committed state on failed disk operations * style(workspace): format review regression request builders * fix(workspace): preserve committed state and verify directory identity * refactor: satisfy quality gates while retaining regression coverage * 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 fr…
#564) * Constrain workspace persistence to validated local IDs * test(workspace): preserve committed state on failed disk operations * style(workspace): format review regression request builders * fix(workspace): preserve committed state and verify directory identity * refactor: satisfy quality gates while retaining regression coverage * 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 fr…
#570) * Bound terminal sessions, output, and process lifecycle * fix(terminal): release exited shells with inherited output pipes * style(terminal): format the lifecycle regression cases * style(terminal): wrap lifecycle diagnostics and regression assertions * ci: run full validation on security repair branches * refactor: satisfy quality gates while retaining regression coverage * 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). …
…574) * Add handle-relative native filesystem operations and platform checks * Bound process logs with private handle-relative rotation * Scope native boundary CI to process log dependencies * fix(process): keep draining output after native log failures * test(process): verify every byte drains after recording fails * fix(process): bound log retention and repair native file permissions * test(native): cover glibc 2.31 and satisfy unchanged quality gates * ci: avoid pipefail when printing the glibc version * test(native): allow cold PowerShell startup for junction fixture * 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) reap…
…#575) * Bound terminal sessions, output, and process lifecycle * fix(terminal): release exited shells with inherited output pipes * style(terminal): format the lifecycle regression cases * Authenticate IPC peers and enforce process and terminal ownership * Verify terminal processes cannot restore IPC credentials * Wait for the complete terminal readiness marker * fix(ci): configure authenticated IPC validation correctly * Add handle-relative native filesystem operations and platform checks * Bound process logs with private handle-relative rotation * Scope native boundary CI to process log dependencies * fix(process): keep draining output after native log failures * test(process): verify every byte drains after recording fails * style(terminal): wrap lifecycle diagnostics and regression assertions * ci: run full validation on security repair branches * fix(process): bound log retention and repair native file permissions * fix(ipc): authenticate process calls and survive a dead repair client * refactor: satisfy quality gates while retaining regression coverage * test(native): cover glibc 2.31 and satisfy unchanged quality gates * fix(ipc): handle explicit adviser failures without hiding cancellation * ci: avoid pipefail when printing the glibc version * test(ipc): distinguish missing credentials from denied permissions * test(native): allow cold PowerShell startup for junction fixture * 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 as…
* Bound mastery admission, fanout, output, and retained history
* test(mastery): verify concurrent execution admission and reuse
* fix(mastery): report validation and timeout failures and free retry slots
* fix(mastery): keep retry handling within quality limits
* ci: run full validation on security repair branches
* 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 Pr…
…568) * Bound repair analysis, retained actions, and source responses * fix(repair): keep default multibyte history within grpc limit * fix(repair): keep pending proposals from blocking automatic recovery * refactor: satisfy quality gates while retaining regression coverage * test(repair): retain a failure outcome for oversized proposals * 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 fr…
#573) * Bound filesystem reads, scans, and watch registrations * Verify filesystem limits across desktop platforms * Exercise persistent Windows watch errors with an exclusive directory handle * fix(filesystem): preserve Windows extended-path reads * fix(filesystem): preserve bounded reads through authorized file links * ci: run full validation on security repair branches * test(filesystem): separate watch delivery deadlines from Windows fixture IO * fix(filesystem): use ARM64 no-follow flag for bounded reads * test(filesystem): pin native leaf-link refusal * 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 (Atomi…
* Authenticate IPC peers and enforce process and terminal ownership
* Verify terminal processes cannot restore IPC credentials
* Wait for the complete terminal readiness marker
* fix(ci): configure authenticated IPC validation correctly
* Bound filesystem reads, scans, and watch registrations
* Verify filesystem limits across desktop platforms
* Exercise persistent Windows watch errors with an exclusive directory handle
* fix(filesystem): preserve Windows extended-path reads
* fix(filesystem): preserve bounded reads through authorized file links
* ci: run full validation on security repair branches
* fix(ipc): authenticate process calls and survive a dead repair client
* fix(filesystem): refuse non-host callers at the file service boundary
* test: run the filesystem service under the authenticated IPC workflow
* ci: trigger the authenticated IPC workflow from the filesystem authorization branch
* Use shared filesystem authorization and verify watch revocation
* test(filesystem): pin RPC guards and quiescent watch revocation
* style(filesystem): format authorization coverage assertions
* test(filesystem): separate watch delivery deadlines from Windows fixture IO
* fix(ipc): handle explicit adviser failures without hiding cancellation
* fix(ipc): handle explicit adviser failures without hiding cancellation
* test(ipc): distinguish missing credentials from denied permissions
* test(ipc): distinguish missing credentials from denied permissions
* fix(filesystem): use ARM64 no-follow flag for bounded reads
* test(filesystem): pin native leaf-link refusal
* 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 o…
…ter CI (#776) * fix(workspace): preserve committed state and verify directory identity (#564) * Constrain workspace persistence to validated local IDs * test(workspace): preserve committed state on failed disk operations * style(workspace): format review regression request builders * fix(workspace): preserve committed state and verify directory identity * refactor: satisfy quality gates while retaining regression coverage * 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 whi…
Summary
Require verified caller identity for all six RunConfiguration RPCs. Execute resolves the requested nonblank id against the provider-held detected configuration and discards caller-supplied command, arguments, directory and environment. Streams capture identity synchronously and revalidate before each emission; idle revoked streams are not proactively disconnected.
A stale Run during/after rescan is refused without executing a replacement configuration. The IPC proxy reports execution failures through the existing run-panel error banner, preserves local errors across host snapshots, and dismisses them on Clear or successful retry. Cancellation propagates from Execute, Scan and Clear. Failure text does not expose remote exception payloads or incorrectly assert that every failure is a stale id.
Credit
Antriksh (@Antriksh1984) authored the original identity gates, provider-held execution design and four original wire tests; also authored the later client-visible lastError/cancellation repair and two proxy tests. Original author commits remain intact.
Previous reviewers added unary/live-stream revocation tests, audit/refusal diagnostics, blank-id refusal, rescan and forwarded-argument tests, and documentation repairs. Prior reviewers added separate local/host error state, dismissal/retry handling, generic failure wording, and seven proxy regressions. These are agent repairs, not original contributor score credit.
Validation and scope
Contributor replay 4550e5f passed all ten hosted checks. Review integration 940cf69 merges dev 5a2e818 while retaining current baseline documentation. Follow-up agent repair 8cd4fe1 fixes newer scan-error precedence, clears run feedback at scan start, neutralizes failure wording, adds caller-only scan audit and strengthens regressions. Final local validation passes 8 bridge + 12 proxy tests, plus ktlintCheck/detekt in both affected modules. All ten hosted checks now pass on final head 8cd4fe1 (run 34838505127); downloaded Linux XML confirms 8 bridge + 12 proxy tests with no failures, errors or skips. Latest Claude findings were reconciled against the repair; current dev browser swipe changes do not touch the bridge, adapter, dependencies or build configuration. These follow-up repairs are agent work, excluded from the original contributor score.
Authorization belongs at the host bridge; client error handling belongs in the published IPC adapter. No plugin API/proto change, runtime publication, app run or live database change. Shared transport/helper/runtime compatibility work (#575) and Windows run disambiguation (#653) remain separate. No UI layout change; no native visual exercise claimed.