Repository navigation
fix(kernel): require a verified caller identity for the secrets and RBAC IPC bridges - #505
Conversation
…BAC IPC bridges BossConsole#53 built a ServerInterceptor (ProcessIdentityInterceptor / ProcessTokenRegistry) that verifies which process is calling the kernel's out-of-process IPC surface, independent of anything a request body claims - but only wired it into PluginUIServiceBridge. The other fourteen service bridges, documented in that interceptor's own KDoc as "none of them authenticated today", still trust every caller unconditionally. SecretServiceBridge and RoleManagementServiceBridge are the two where that actually matters: the whole password vault (list/search/create/update/ delete/share/unshare) and RBAC (roles, permissions, grants) had no caller attribution at all. Any out-of-process plugin that can reach the kernel's local IPC socket could read, mutate or share every secret, or mint and revoke permission grants, with nothing to tell one plugin's calls from another's. Both bridges now require AUTHENTICATED_PROCESS_ID on every RPC - reads included, not only mutations, since an unattributed read of the vault list is exactly the exposure this closes - and refuse with PERMISSION_DENIED before the request ever reaches the underlying provider when there is none. Every mutating call also logs the verified caller identity, so a compromised or rogue plugin's activity is attributable after the fact. Every legitimate out-of-process plugin already carries a valid credential in production (ChildProcessBootstrap attaches ProcessTokenClientInterceptor whenever BOSS_PROCESS_TOKEN is set, which ProcessSpawner sets for every child it spawns), so this closes the gap without changing behavior for any real plugin. Tests exercise both bridges over a real gRPC server and real client, mirroring PluginUIServiceBridgeTest's own harness: an authenticated caller succeeds, an anonymous one is refused, and - the assertion that actually matters - the underlying provider is never invoked on a refusal.
4eea05a to
ed41e36
Compare
|
Reopening to retrigger CI after rebase onto current dev. |
|
@Antriksh1984 — reviewed against current Housekeeping note: Validation on the final head: ktlint, detekt, and compile all pass; @claude Please review current head |
|
Claude finished @kshivang's task in 4m 53s —— View job Review: caller-identity gating at
|
…ose the updateSecret gap - SecretServiceBridgeTest: updateSecret gets its own anonymous-refusal test, matching the vault mutations it already covered. - SecretServiceBridgeTest: a service registered on an already-started server (BossIpcServer's late-service fallback registry, as KernelBootstrap.registerPluginServices does) is still gated by ProcessIdentityInterceptor - anonymous calls are refused before the vault is reached, token calls go through. This pins the registration order (fallbackHandlerRegistry before intercept) the production path relies on. - KDoc on both authenticatedCallerOrRefuse helpers: the AUTHENTICATED_PROCESS_ID snapshot is correct for unary RPCs; a streaming bridge must read CURRENT_IDENTITY instead (see PluginUIServiceBridge.streamUI).
|
@Antriksh1984 Follow-up after the Claude review: all three coverage findings are now addressed in What changed
Validation
The gating logic itself is unchanged — this pass only adds the missing regression coverage and the doc note. @claude please re-review at |
|
Claude finished @kshivang's task in 3m 24s —— View job Re-review:
|
…en coverage, fix stale KDocs RoleManagementServiceBridgeTest: getRolePermissions, validateRoleName and validatePermissionName were asserted only as "some StatusException", with no counters on FakeRoleManagementProvider at all - a bare assertFailsWith<StatusException> would pass just as happily on UNAVAILABLE or UNIMPLEMENTED, contradicting the class's own KDoc claim that every refusal test proves the provider was never reached. Added the three missing counters and now assert PERMISSION_DENIED plus a zero counter on every refusal in both bridge test files, not just the two "an anonymous caller is refused" tests. SecretServiceBridgeTest: the late-registration test called tokenRegistry.issue(DEFAULT_PROCESS) for the same id setUp already issued for - issue() replaces and invalidates the previous token by design, so this silently killed the credential the shared `authenticated` stub carries for the rest of the class. Now issues a distinct id. lateServer teardown moved into a finally so a failed assertion doesn't leak a bound server. Moved an orphaned KDoc comment back onto FakeSecretDataProvider (SingleServiceRegistry's insertion had left it dangling above the wrong class). Added a revoked-token test to both files: identityFor returns null for a token the registry no longer holds, the post-reapChildren/post-respawn case, previously untested - every existing refusal test only covered "no credential at all", not "a credential that was live and got pulled". Two stale KDoc facts fixed in boss-ipc: ProcessIdentityInterceptor's comment named PluginUIServiceBridge as the only bridge checking identity (now three of fifteen, with SecretServiceBridge and RoleManagementServiceBridge named); BossIpcServer's tokenRegistry KDoc claimed a null registry "leaves every service's behaviour exactly as it was", which stopped being true for a server hosting either of the two fail-closed bridges - a null registry there means every call refused, not merely unauthenticated as before.
|
Addressed finding 1 (still fully open) and both items from the follow-up review, pushed on `081bf2a1`: Finding 1 - RBAC refusal assertions. Added the three missing counters (`getRolePermissionsCalls`, `validateRoleNameCalls`, `validatePermissionNameCalls`) to `FakeRoleManagementProvider` and now assert both `Status.Code.PERMISSION_DENIED` and a zero counter on every refusal in both `RoleManagementServiceBridgeTest` and `SecretServiceBridgeTest` - not just the two `an anonymous caller is refused` tests. 21/21 refusal assertions now pin the actual status code. Follow-up finding 2 - the shared token bug + revoked-token gap. The late-registration test's `tokenRegistry.issue(DEFAULT_PROCESS)` was silently invalidating the `authenticated` stub's credential for the rest of the class (harmless today only because nothing after it used that stub). Now issues a distinct id. Also added a real revoked-token test to both files: issue a token, confirm it works, `tokenRegistry.revoke(...)`, assert `PERMISSION_DENIED` and the counter stays put - the post-reapChildren/post-respawn case, previously only constructed by accident and never asserted. Follow-up finding 4 (nit). Moved the orphaned `FakeSecretDataProvider` KDoc back onto the class it describes, and `lateServer` teardown is now in a `finally`. Both stale KDoc facts. `ProcessIdentityInterceptor`'s comment now says "twelve other service bridges... none of them authenticated" and names all three that do check identity (`PluginUIServiceBridge`, `SecretServiceBridge`, `RoleManagementServiceBridge`). `BossIpcServer`'s `tokenRegistry` KDoc now says explicitly that a null registry stops being a no-op once a fail-closed bridge is hosted on it - every call refused, not "unauthenticated as before." Not addressed this round (as you framed it, reasonable to leave for now):
Happy to pick up finding 3 or the DRY extraction as a follow-up if you'd like them closed rather than left as known gaps. Validation: `compileKotlinDesktop`/`compileTestKotlinDesktop`/`boss-ipc:compileKotlin` clean, both bridge test suites green (`SecretServiceBridgeTest` 12/12, `RoleManagementServiceBridgeTest` 11/11 including the two new revoked-token tests), `ktlintCheck`/`detekt` clean on every file this commit touches. |
|
@Antriksh1984 Status at Record correction (my earlier comment 5633174606). I wrote that "all three coverage findings" were addressed in Validation on
Finding state at head:
No author-side blockers remain that I can see; what's left on this PR is review sign-off. @claude please re-review at |
|
Claude finished @kshivang's task in 2m 34s —— View job Re-review:
|
…r KDoc The kernel registers 16 bridges on its ipcServer (15 in KernelBootstrap.registerPluginServices plus PluginUIServiceBridge at KernelBootstrap.kt:475); three authenticate, so thirteen are unauthenticated, not twelve. Per the Claude review's one-word count fix.
|
Follow-up to the re-review: applied the one change the bottom line called out - the KDoc count in Validation at The agreed follow-ups (real- |
…BAC IPC bridges (#505) * fix(kernel): require a verified caller identity for the secrets and RBAC IPC bridges BossConsole#53 built a ServerInterceptor (ProcessIdentityInterceptor / ProcessTokenRegistry) that verifies which process is calling the kernel's out-of-process IPC surface, independent of anything a request body claims - but only wired it into PluginUIServiceBridge. The other fourteen service bridges, documented in that interceptor's own KDoc as "none of them authenticated today", still trust every caller unconditionally. SecretServiceBridge and RoleManagementServiceBridge are the two where that actually matters: the whole password vault (list/search/create/update/ delete/share/unshare) and RBAC (roles, permissions, grants) had no caller attribution at all. Any out-of-process plugin that can reach the kernel's local IPC socket could read, mutate or share every secret, or mint and revoke permission grants, with nothing to tell one plugin's calls from another's. Both bridges now require AUTHENTICATED_PROCESS_ID on every RPC - reads included, not only mutations, since an unattributed read of the vault list is exactly the exposure this closes - and refuse with PERMISSION_DENIED before the request ever reaches the underlying provider when there is none. Every mutating call also logs the verified caller identity, so a compromised or rogue plugin's activity is attributable after the fact. Every legitimate out-of-process plugin already carries a valid credential in production (ChildProcessBootstrap attaches ProcessTokenClientInterceptor whenever BOSS_PROCESS_TOKEN is set, which ProcessSpawner sets for every child it spawns), so this closes the gap without changing behavior for any real plugin. Tests exercise both bridges over a real gRPC server and real client, mirroring PluginUIServiceBridgeTest's own harness: an authenticated caller succeeds, an anonymous one is refused, and - the assertion that actually matters - the underlying provider is never invoked on a refusal. * test(kernel): pin the identity gate on late-registered bridges and close the updateSecret gap - SecretServiceBridgeTest: updateSecret gets its own anonymous-refusal test, matching the vault mutations it already covered. - SecretServiceBridgeTest: a service registered on an already-started server (BossIpcServer's late-service fallback registry, as KernelBootstrap.registerPluginServices does) is still gated by ProcessIdentityInterceptor - anonymous calls are refused before the vault is reached, token calls go through. This pins the registration order (fallbackHandlerRegistry before intercept) the production path relies on. - KDoc on both authenticatedCallerOrRefuse helpers: the AUTHENTICATED_PROCESS_ID snapshot is correct for unary RPCs; a streaming bridge must read CURRENT_IDENTITY instead (see PluginUIServiceBridge.streamUI). * test(kernel): tighten RBAC/secret refusal assertions, add revoked-token coverage, fix stale KDocs RoleManagementServiceBridgeTest: getRolePermissions, validateRoleName and validatePermissionName were asserted only as "some StatusException", with no counters on FakeRoleManagementProvider at all - a bare assertFailsWith<StatusException> would pass just as happily on UNAVAILABLE or UNIMPLEMENTED, contradicting the class's own KDoc claim that every refusal test proves the provider was never reached. Added the three missing counters and now assert PERMISSION_DENIED plus a zero counter on every refusal in both bridge test files, not just the two "an anonymous caller is refused" tests. SecretServiceBridgeTest: the late-registration test called tokenRegistry.issue(DEFAULT_PROCESS) for the same id setUp already issued for - issue() replaces and invalidates the previous token by design, so this silently killed the credential the shared `authenticated` stub carries for the rest of the class. Now issues a distinct id. lateServer teardown moved into a finally so a failed assertion doesn't leak a bound server. Moved an orphaned KDoc comment back onto FakeSecretDataProvider (SingleServiceRegistry's insertion had left it dangling above the wrong class). Added a revoked-token test to both files: identityFor returns null for a token the registry no longer holds, the post-reapChildren/post-respawn case, previously untested - every existing refusal test only covered "no credential at all", not "a credential that was live and got pulled". Two stale KDoc facts fixed in boss-ipc: ProcessIdentityInterceptor's comment named PluginUIServiceBridge as the only bridge checking identity (now three of fifteen, with SecretServiceBridge and RoleManagementServiceBridge named); BossIpcServer's tokenRegistry KDoc claimed a null registry "leaves every service's behaviour exactly as it was", which stopped being true for a server hosting either of the two fail-closed bridges - a null registry there means every call refused, not merely unauthenticated as before. * docs(ipc): correct the unauthenticated bridge count in the interceptor KDoc The kernel registers 16 bridges on its ipcServer (15 in KernelBootstrap.registerPluginServices plus PluginUIServiceBridge at KernelBootstrap.kt:475); three authenticate, so thirteen are unauthenticated, not twelve. Per the Claude review's one-word count fix. --------- Co-authored-by: kshivang <kshivang@users.noreply.github.com>
Every ActiveTabsService RPC was reachable by any process able to open a connection to the kernel IPC server, not only the plugins the host itself loaded. watchActiveTabs disclosed every open tab's title and URL across every window with no credential at all, and createBrowserTab could force the browser to navigate to an arbitrary URL; selectTab/closeTab could manipulate what the user is looking at. All seven RPCs now require a verified caller identity, matching the pattern PR risa-labs-inc#505 established for the Secret Service: unary RPCs call authenticatedCallerOrRefuse, and the watchActiveTabs stream captures ProcessIdentityInterceptor.CURRENT_IDENTITY synchronously in the method body (before the flow is collected, on the calling thread the interceptor's context is current on) and re-checks it before every emission, so a revoked caller's stream is refused before an already-held handle could receive a value it should no longer see. Refusals are logged with a bind/revocation stage tag, and the three mutating RPCs (selectTab, createBrowserTab, closeTab) now record an attributed audit line - caller and RPC name only, no URLs or titles. Tested with a real gRPC server plus ProcessIdentityInterceptor: every RPC is refused with no credential and never reaches the provider, an authenticated caller can watch/read/select/create/close tabs, and a revoked caller's live watch stream terminates with PERMISSION_DENIED before receiving a value that arrived after revocation.
…on bridge Every RunConfigurationService RPC was reachable by any process able to open a connection to the kernel IPC server. watchDetectedConfigurations disclosed every auto-detected run configuration (file paths, commands, arguments, environment variables) with no credential, and execute was an unauthenticated arbitrary-command-execution primitive: ExecuteConfigRequest carries a full command, argument string, working directory and environment map that RunConfigurationDataProvider.execute hands straight to RunExecutionService, whose own KDoc says what that does - "opens a terminal and runs the command." All six RPCs now require a verified caller identity, matching the pattern PR risa-labs-inc#505 established for the Secret Service. execute gets a second, narrower guard on top of identity: it does not run what the caller sent. It takes only the requested id, looks up the matching entry in the provider's own detectedConfigurations, and executes that provider-held copy - so a request naming a real id but carrying a tampered command, or naming an id outside the provider's current set (including one from before a rescan, since scanning clears the list and mints fresh ids), is refused with PERMISSION_DENIED before RunExecutionService ever sees it. A blank id is refused outright rather than matching a stray blank-id entry. Tested with a real gRPC server: every RPC is refused with no credential; an authenticated caller can watch/scan/clear errors; execute runs the provider's own stored command even when the request carries a tampered one; an unknown or blank id is refused and never reaches RunExecutionService; a revoked caller's live watch stream terminates before a later snapshot; and an id observed before a rescan is refused both during and after it.
…dge (#646) Every ActiveTabsService RPC was reachable by any process able to open a connection to the kernel IPC server, not only the plugins the host itself loaded. watchActiveTabs disclosed every open tab's title and URL across every window with no credential at all, and createBrowserTab could force the browser to navigate to an arbitrary URL; selectTab/closeTab could manipulate what the user is looking at. All seven RPCs now require a verified caller identity, matching the pattern PR #505 established for the Secret Service: unary RPCs call authenticatedCallerOrRefuse, and the watchActiveTabs stream captures ProcessIdentityInterceptor.CURRENT_IDENTITY synchronously in the method body (before the flow is collected, on the calling thread the interceptor's context is current on) and re-checks it before every emission, so a revoked caller's stream is refused before an already-held handle could receive a value it should no longer see. Refusals are logged with a bind/revocation stage tag, and the three mutating RPCs (selectTab, createBrowserTab, closeTab) now record an attributed audit line - caller and RPC name only, no URLs or titles. Tested with a real gRPC server plus ProcessIdentityInterceptor: every RPC is refused with no credential and never reaches the provider, an authenticated caller can watch/read/select/create/close tabs, and a revoked caller's live watch stream terminates with PERMISSION_DENIED before receiving a value that arrived after revocation.
…dge (#646) Every ActiveTabsService RPC was reachable by any process able to open a connection to the kernel IPC server, not only the plugins the host itself loaded. watchActiveTabs disclosed every open tab's title and URL across every window with no credential at all, and createBrowserTab could force the browser to navigate to an arbitrary URL; selectTab/closeTab could manipulate what the user is looking at. All seven RPCs now require a verified caller identity, matching the pattern PR #505 established for the Secret Service: unary RPCs call authenticatedCallerOrRefuse, and the watchActiveTabs stream captures ProcessIdentityInterceptor.CURRENT_IDENTITY synchronously in the method body (before the flow is collected, on the calling thread the interceptor's context is current on) and re-checks it before every emission, so a revoked caller's stream is refused before an already-held handle could receive a value it should no longer see. Refusals are logged with a bind/revocation stage tag, and the three mutating RPCs (selectTab, createBrowserTab, closeTab) now record an attributed audit line - caller and RPC name only, no URLs or titles. Tested with a real gRPC server plus ProcessIdentityInterceptor: every RPC is refused with no credential and never reaches the provider, an authenticated caller can watch/read/select/create/close tabs, and a revoked caller's live watch stream terminates with PERMISSION_DENIED before receiving a value that arrived after revocation.
…on bridge Every RunConfigurationService RPC was reachable by any process able to open a connection to the kernel IPC server. watchDetectedConfigurations disclosed every auto-detected run configuration with no credential, and execute was an unauthenticated arbitrary-command-execution primitive: ExecuteConfigRequest carries a full command, argument string, working directory and environment map that RunConfigurationDataProvider.execute hands straight to RunExecutionService, whose own KDoc says what that does - "opens a terminal and runs the command." All six RPCs now require a verified caller identity, matching the pattern PR risa-labs-inc#505 established for the Secret Service. execute does not run what the caller sent: it takes only the requested id, looks up the matching entry in the provider's own detectedConfigurations, and executes that provider-held copy - so a request naming a real id but carrying a tampered command, or naming an id outside the provider's current set (including a blank id, or one from before a rescan), is refused with PERMISSION_DENIED before RunExecutionService ever sees it. The client-side proxy (RunConfigDataProviderProxy) previously caught every exception from a refused execute and discarded it silently, so a Run click during or just after a rescan resolved to nothing on either side - no terminal, no error, only a host-log line nobody was looking at. execute now surfaces a client-visible refusal through lastError, the one signal RunConfigurationDataProvider has and that the run panel already renders, kept separate from the host's own scan-error channel so the two lifetimes don't clobber each other: a local execution error clears on a successful retry or an explicit clearError(), a host error is preserved underneath it and restored once the local one clears, and the message stays generic rather than claiming a specific cause (stale id vs. a revoked token vs. a transport fault) it cannot actually distinguish safely. Cancellation now propagates untouched from all three RPCs rather than being swallowed by a blanket catch. Tested with a real gRPC server against the bridge (8 cases: identity gating on all six RPCs, tamper resistance, unknown/blank id refusal, unary and mid-stream revocation, refusal during and after a rescan) and against the proxy with a fake service (9 cases: refused vs. successful execute, host/local error independence, dismiss and retry clearing, transport failure not claiming a stale id, and cancellation propagating from all three RPCs without publishing a spurious error).
…d service bridges NotificationServiceBridge, ContextMenuServiceBridge, DirectoryPickerServiceBridge, PanelEventServiceBridge and PerformanceServiceBridge accepted any authenticated process with no per-call attribution, while their siblings already enforce the documented verified-caller-identity pattern (BossConsole#53, introduced for the Secret Service in PR risa-labs-inc#505). A spawned process could spoof toasts, inject context-menu registrations, drive the native directory dialog, rearrange panels and forge performance mutations with nothing in the log to attribute them to. - Add the sibling AUTHENTICATED_PROCESS_ID helper (mirror + KDoc citing risa-labs-inc#53/risa-labs-inc#505) to all five bridges; unary RPCs refuse with PERMISSION_DENIED when no verified identity is present, even with no interceptor in front of them. - Performance watchers capture CURRENT_IDENTITY at subscription and revalidate the caller on every emission, the same mid-stream revocation shape LogServiceBridge uses. - ContextMenuServiceBridge keeps its passive acknowledge-and-drop contract (issue risa-labs-inc#30) for verified callers; only unattributed callers change behaviour. - New UnattributedBridgesIdentityTest: all five services over a real gRPC server with the production interceptor (no credential -> UNAUTHENTICATED, providers never reached), authenticated calls reach each provider, and direct calls with no identity context fail closed with PERMISSION_DENIED even without the interceptor. - ContextMenuServiceBridgeTest now pins the refusal half of the risa-labs-inc#53 contract directly. Gates: ktlintCheck + detekt + :composeApp:desktopTest --tests "ai.rever.boss.kernel.services.*" in one invocation - 13 suites, 104 tests, 0 failures, 0 errors, 0 skipped. Fixes risa-labs-inc#1218
…d service bridges NotificationServiceBridge, ContextMenuServiceBridge, DirectoryPickerServiceBridge, PanelEventServiceBridge and PerformanceServiceBridge accepted any authenticated process with no per-call attribution, while their siblings already enforce the documented verified-caller-identity pattern (BossConsole#53, introduced for the Secret Service in PR risa-labs-inc#505). A spawned process could spoof toasts, inject context-menu registrations, drive the native directory dialog, rearrange panels and forge performance mutations with nothing in the log to attribute them to. - Add the sibling AUTHENTICATED_PROCESS_ID helper (mirror + KDoc citing risa-labs-inc#53/risa-labs-inc#505) to all five bridges; unary RPCs refuse with PERMISSION_DENIED when no verified identity is present, even with no interceptor in front of them. - Performance watchers capture CURRENT_IDENTITY at subscription and revalidate the caller on every emission, the same mid-stream revocation shape LogServiceBridge uses. - ContextMenuServiceBridge keeps its passive acknowledge-and-drop contract (issue risa-labs-inc#30) for verified callers; only unattributed callers change behaviour. - New UnattributedBridgesIdentityTest: all five services over a real gRPC server with the production interceptor (no credential -> UNAUTHENTICATED, providers never reached), authenticated calls reach each provider, and direct calls with no identity context fail closed with PERMISSION_DENIED even without the interceptor. - ContextMenuServiceBridgeTest now pins the refusal half of the risa-labs-inc#53 contract directly. Gates: ktlintCheck + detekt + :composeApp:desktopTest --tests "ai.rever.boss.kernel.services.*" in one invocation - 13 suites, 104 tests, 0 failures, 0 errors, 0 skipped. Fixes risa-labs-inc#1218
…d service bridges (#1339) NotificationServiceBridge, ContextMenuServiceBridge, DirectoryPickerServiceBridge, PanelEventServiceBridge and PerformanceServiceBridge accepted any authenticated process with no per-call attribution, while their siblings already enforce the documented verified-caller-identity pattern (BossConsole#53, introduced for the Secret Service in PR #505). A spawned process could spoof toasts, inject context-menu registrations, drive the native directory dialog, rearrange panels and forge performance mutations with nothing in the log to attribute them to. - Add the sibling AUTHENTICATED_PROCESS_ID helper (mirror + KDoc citing #53/#505) to all five bridges; unary RPCs refuse with PERMISSION_DENIED when no verified identity is present, even with no interceptor in front of them. - Performance watchers capture CURRENT_IDENTITY at subscription and revalidate the caller on every emission, the same mid-stream revocation shape LogServiceBridge uses. - ContextMenuServiceBridge keeps its passive acknowledge-and-drop contract (issue #30) for verified callers; only unattributed callers change behaviour. - New UnattributedBridgesIdentityTest: all five services over a real gRPC server with the production interceptor (no credential -> UNAUTHENTICATED, providers never reached), authenticated calls reach each provider, and direct calls with no identity context fail closed with PERMISSION_DENIED even without the interceptor. - ContextMenuServiceBridgeTest now pins the refusal half of the #53 contract directly. Gates: ktlintCheck + detekt + :composeApp:desktopTest --tests "ai.rever.boss.kernel.services.*" in one invocation - 13 suites, 104 tests, 0 failures, 0 errors, 0 skipped. Fixes #1218
Summary
BossConsole#53 built a full caller-identity verification system for the kernel's out-of-process IPC transport -
ProcessIdentityInterceptor(aServerInterceptor) plusProcessTokenRegistry(mints a per-process credential at spawn, never trusts a request body). It's wired intoBossIpcServerin production andPluginUIServiceBridgeuses it correctly, with real per-surface ownership checks inRemoteUiSurfaceRegistry.But
ProcessIdentityInterceptor's own KDoc says it plainly: "fourteen other service bridges as of writing, none of them authenticated today." The interceptor establishes identity on every call; it's each service's own choice whether to check it, and only one of fifteen bridges makes that choice.This PR closes that gap for the two bridges where it matters most:
SecretServiceBridge- the entire out-of-process password vault surface: list, search, create, update, delete, share, unshare.RoleManagementServiceBridge- RBAC: list/create/delete roles and permissions, grant/revoke a permission on a role.Before this, any out-of-process plugin that could reach the kernel's local IPC socket had unrestricted, unattributed access to both. Not a network-remote threat (the socket is user-scoped), but a real gap against a rogue or compromised plugin process - exactly the class of concern #53 raised, on a more sensitive surface than the UI transport it originally fixed.
What changed
Both bridges now call an
authenticatedCallerOrRefuse(rpc)helper (mirroringPluginUIServiceBridge's own) at the top of every RPC method - reads included, not only mutations, since an unattributed read of the secret list is exactly the exposure this closes. A call with no verifiedAUTHENTICATED_PROCESS_IDin its gRPCContextis refused withPERMISSION_DENIEDbefore it ever reaches the underlyingSecretDataProvider/RoleManagementProvider. Every mutating call additionally logs the verified caller identity (LogCategory.AUTH), so a compromised plugin's vault/RBAC activity is attributable after the fact rather than just blocked.No behavior change for any real plugin. Every process spawned through
ai.rever.boss.process.ProcessSpawner(the only spawn path in production) already receives aBOSS_PROCESS_TOKENenv var, andChildProcessBootstrapalready attachesProcessTokenClientInterceptorto its kernel channel whenever that token is present. The only callers with no credential are processes started outside that path - tests, and exactly the kind of caller this closes the door on.Test plan
New
SecretServiceBridgeTest(9 tests) andRoleManagementServiceBridgeTest(10 tests), both exercised over a real gRPC server and real client - same harnessPluginUIServiceBridgeTestalready established (ProcessIdentityInterceptor+ProcessTokenRegistryon a realServerBuilder,ProcessTokenClientInterceptoron a realManagedChannel, ananonymousCaller()with no credential at all).Every refusal test asserts two things: the RPC fails with
PERMISSION_DENIED, and the underlying provider's call counter stayed at zero - the assertion that actually matters, since a refusal that still reached the vault would defeat the point../gradlew :composeApp:compileKotlinDesktop :composeApp:compileTestKotlinDesktop- clean./gradlew :composeApp:desktopTest --tests "ai.rever.boss.kernel.services.*"- all green, including the pre-existingPluginUIServiceBridgeTestsuite (no regression)./gradlew :boss-ipc:test- clean./gradlew ktlintCheck detekt- cleanDeliberately out of scope
The other twelve unauthenticated bridges (
ActiveTabsServiceBridge,ContextMenuServiceBridge,DirectoryPickerServiceBridge,DownloadServiceBridge,GitServiceBridge,LogServiceBridge,NotificationServiceBridge,PanelEventServiceBridge,PerformanceServiceBridge,ProjectDataServiceBridge,RunConfigServiceBridge,SplitViewServiceBridge,SupabaseServiceBridge) are untouched. Their worst case is stale/crossed UI or workspace state rather than credential/permission exposure, and extending the pattern to all of them is a much larger, more mechanical follow-up better done as its own PR (or several) rather than bundled here.Duplicate submission provenance
This PR is the surviving implementation for #598, also authored by Antriksh (@Antriksh1984). Both submissions gate the same nine Secret Service RPCs before provider access using the same verified identity and PERMISSION_DENIED result. #505 additionally retains attributed mutation logging, RBAC protection, individual refusal tests, token-revocation coverage and late-service registration coverage. #598 was reviewed against #505 at
7bf4f6e0db504495cfebf6e76ba552933d2cce0c; it supplies no additional production behavior. Its positive workflow test exercises existing provider mapping rather than a distinct fix. Contributor credit belongs to Antriksh for both original submissions; prior reviewer repairs remain separately attributed above. No code from another contributor is being dropped.Final source-level runtime check: at runtime main
7ac0607ee7884b04a4b225bbdc3cf732c83cb30f, PluginProcessMain passes connection.kernelClient.channel into RemotePluginContext, whose SecretDataProviderProxy and RoleManagementProviderProxy reuse it. This resolves the review's channel-wiring question without claiming packaged-runtime validation. Head7bf4f6e0dremains unchanged with all nine CI checks green and prior focused tests/quality checks passing. User authorized a serialized squash merge into dev only.