feat: persist permission auto-accept on server (#2158)

Move per-session permission auto-accept policy ownership from the UI to the
OpenChamber server so enabled sessions continue running when clients disconnect
or the server restarts.

- persist explicit per-session policies in OpenChamber settings
- inherit the nearest explicit policy across subagent session hierarchies
- allow child sessions to opt out of an inherited parent policy
- immediately accept matching global and directory-scoped pending requests
- process future requests without requiring a connected UI client
- reconcile pending permissions after startup and event-stream reconnects
- deduplicate concurrent requests and retry transient reply failures
- synchronize policy updates across connected clients
- migrate existing browser-persisted policies to server storage
- suppress auto-accepted permission cards before they enter UI state
- show deduplicated permission toasts for inactive sessions
- preserve foreground-only permission handling in VS Code
- integrate directory-aware notification routing from main
- add coverage for persistence, inheritance, retries, reconciliation, pending
  requests, client hydration, and inactive-session toasts
This commit is contained in:
Bohdan Triapitsyn
2026-07-12 15:03:16 +03:00
committed by GitHub
parent 3d90eddcaf
commit d738d41574
18 changed files with 795 additions and 387 deletions
@@ -15,6 +15,10 @@ export const createNotificationTriggerRuntime = (deps) => {
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
} = deps;
let getIsSessionAutoAccepting = deps.getIsSessionAutoAccepting;
const setGetIsSessionAutoAccepting = (resolver) => {
getIsSessionAutoAccepting = typeof resolver === 'function' ? resolver : undefined;
};
// App-icon badge for native push: the set of DISTINCT collapse-ids (the push
// `tag`, e.g. `ready-<sessionId>` / `permission-<requestKey>`) we've sent since
@@ -609,7 +613,8 @@ export const createNotificationTriggerRuntime = (deps) => {
// Client may be in Permission Auto-Accept for this session (or any
// ancestor). Skip the whole notification path — the client responds
// directly and the user has opted out of approval prompts.
if (await isSessionAutoAccepting(sessionId, notificationDirectory)) {
if (await (getIsSessionAutoAccepting?.(sessionId, notificationDirectory)
?? isSessionAutoAccepting(sessionId, notificationDirectory))) {
if (requestKey) notifiedPermissionRequests.add(requestKey);
return;
}
@@ -622,7 +627,8 @@ export const createNotificationTriggerRuntime = (deps) => {
const timer = setTimeout(async () => {
pushPermissionDebounceTimers.delete(sessionId);
if (await isSessionAutoAccepting(sessionId, notificationDirectory)) {
if (await (getIsSessionAutoAccepting?.(sessionId, notificationDirectory)
?? isSessionAutoAccepting(sessionId, notificationDirectory))) {
if (requestKey) notifiedPermissionRequests.add(requestKey);
return;
}
@@ -742,6 +748,7 @@ export const createNotificationTriggerRuntime = (deps) => {
maybeSendPushForTrigger,
setAutoAcceptSession,
setGetIsWindowFocused,
setGetIsSessionAutoAccepting,
clearPendingPushBadge,
sendGoalSettlePush,
};