feat: add secure multi-runtime controls

Make agent backends configurable and governable with encrypted credentials, explicit context sharing, and tool approval boundaries.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-07-30 10:52:35 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent 1b6178b841
commit 698a15ad14
27 changed files with 2465 additions and 91 deletions
+95 -10
View File
@@ -2,17 +2,35 @@ import { app, BrowserWindow, ipcMain } from 'electron'
import { z } from 'zod'
import {
agentRequestSchema,
runtimeSettingsInputSchema,
type AgentEvent,
type AppInfo
type AppInfo,
type RuntimeSettings
} from '../shared/contracts'
import { ipcChannels } from '../shared/ipc-channels'
import type { AgentRuntime } from './agent/runtime'
import type { ContextManager } from './context-manager'
import type { RuntimeSettingsStore } from './runtime-settings-store'
import type { ToolApprovalBroker } from './tool-approval-broker'
import { showWindow } from './window'
const requestIdSchema = z.string().uuid()
const approvalResponseSchema = z
.object({
approvalId: z.string().uuid(),
approved: z.boolean()
})
.strict()
function assertTrustedSender(event: Electron.IpcMainInvokeEvent, window: BrowserWindow): void {
if (event.sender !== window.webContents) {
function assertTrustedSender(
event: Electron.IpcMainInvokeEvent,
window: BrowserWindow
): void {
if (
event.sender !== window.webContents ||
event.senderFrame !== window.webContents.mainFrame ||
event.senderFrame.url !== window.webContents.getURL()
) {
throw new Error('拒绝来自未知窗口的 IPC 请求')
}
}
@@ -20,7 +38,12 @@ function assertTrustedSender(event: Electron.IpcMainInvokeEvent, window: Browser
export function registerIpcHandlers(
window: BrowserWindow,
runtime: AgentRuntime,
shortcut: string
shortcut: string,
settingsStore: RuntimeSettingsStore,
contextManager: ContextManager,
approvalBroker: ToolApprovalBroker,
defaultWorkspace: string,
onRuntimeSettingsChanged: () => Promise<void>
): () => void {
const activeRequests = new Map<string, AbortController>()
const channels = Object.values(ipcChannels).filter(
@@ -33,6 +56,13 @@ export function registerIpcHandlers(
ipcMain.removeHandler(channel)
}
const abortActiveRequests = (reason: string): void => {
for (const controller of activeRequests.values()) {
controller.abort(new Error(reason))
}
activeRequests.clear()
}
ipcMain.handle(ipcChannels.appInfo, (event): AppInfo => {
assertTrustedSender(event, window)
return {
@@ -61,7 +91,7 @@ export function registerIpcHandlers(
ipcMain.handle(ipcChannels.agentRun, (event, input: unknown) => {
assertTrustedSender(event, window)
const request = agentRequestSchema.parse(input)
const request = contextManager.enrichRequest(agentRequestSchema.parse(input))
if (activeRequests.has(request.requestId)) {
throw new Error('请求正在执行')
}
@@ -73,7 +103,27 @@ export function registerIpcHandlers(
try {
for await (const agentEvent of runtime.run(
request,
controller.signal
controller.signal,
async (requiresToolApproval) => {
if (!requiresToolApproval) {
return
}
const settings = await settingsStore.getResolvedSettings()
await approvalBroker.request(
settings.toolApproval,
request.requestId,
defaultWorkspace,
controller.signal,
(approvalEvent) => {
if (!window.isDestroyed()) {
window.webContents.send(
ipcChannels.agentEvent,
approvalEvent
)
}
}
)
}
)) {
if (!window.isDestroyed()) {
window.webContents.send(ipcChannels.agentEvent, agentEvent)
@@ -104,11 +154,46 @@ export function registerIpcHandlers(
activeRequests.get(requestId)?.abort(new Error('用户取消了请求'))
})
return () => {
for (const controller of activeRequests.values()) {
controller.abort(new Error('应用正在退出'))
ipcMain.handle(ipcChannels.agentApprovalRespond, (event, input: unknown) => {
assertTrustedSender(event, window)
const response = approvalResponseSchema.parse(input)
approvalBroker.respond(response.approvalId, response.approved)
})
ipcMain.handle(
ipcChannels.runtimeSettingsGet,
(event): Promise<RuntimeSettings> => {
assertTrustedSender(event, window)
return settingsStore.getPublicSettings()
}
activeRequests.clear()
)
ipcMain.handle(
ipcChannels.runtimeSettingsUpdate,
async (event, input: unknown): Promise<RuntimeSettings> => {
assertTrustedSender(event, window)
const settings = runtimeSettingsInputSchema.parse(input)
const savedSettings = await settingsStore.update(settings)
abortActiveRequests('运行时设置已更改')
await onRuntimeSettingsChanged()
return savedSettings
}
)
ipcMain.handle(ipcChannels.contextSelectFiles, (event) => {
assertTrustedSender(event, window)
return contextManager.selectFiles(window)
})
ipcMain.handle(ipcChannels.contextRemove, (event, input: unknown) => {
assertTrustedSender(event, window)
contextManager.remove(requestIdSchema.parse(input))
})
return () => {
abortActiveRequests('应用正在退出')
approvalBroker.clear()
contextManager.clear()
for (const channel of channels) {
ipcMain.removeHandler(channel)
}