From 07f3f1cc36caf8697c3d8302cc4f241d6c63fd92 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 7 Jul 2026 22:10:26 +0800 Subject: [PATCH] refactor: align permission mode injection --- .../agent/permissionGate/permissionGate.ts | 25 ++-------- .../permissionGate/permissionGateService.ts | 45 +++++++++++------ .../injection/permissionModeInjection.ts | 44 +++++++++++------ .../agent/permissionMode/permissionMode.ts | 3 +- .../permissionMode/permissionModeService.ts | 12 ++--- .../permissionMode/permissionMode.test.ts | 49 ++++++++++++++++++- 6 files changed, 116 insertions(+), 62 deletions(-) diff --git a/packages/agent-core-v2/src/agent/permissionGate/permissionGate.ts b/packages/agent-core-v2/src/agent/permissionGate/permissionGate.ts index 3c3c645d1..cbbf19558 100644 --- a/packages/agent-core-v2/src/agent/permissionGate/permissionGate.ts +++ b/packages/agent-core-v2/src/agent/permissionGate/permissionGate.ts @@ -1,30 +1,11 @@ -import type { - ApprovalRequest, - ApprovalResponse, - PermissionData, -} from '#/agent/permissionPolicy/types'; import { createDecorator } from "#/_base/di/instantiation"; +import type { + PermissionData +} from '#/agent/permissionPolicy/types'; import type { AuthorizeToolExecutionResult, ResolvedToolExecutionHookContext, } from '#/agent/tool/toolHooks'; -import type { Hooks } from '#/hooks'; - -export type PermissionApprovalRequestContext = ApprovalRequest & { - readonly sessionId?: string; - readonly agentId?: string; - readonly turnId: number; - readonly toolInput: unknown; -}; - -export type PermissionApprovalResultContext = PermissionApprovalRequestContext & - ( - | ApprovalResponse - | { - readonly decision: 'error'; - readonly error: string; - } - ); export interface IAgentPermissionGate { readonly _serviceBrand: undefined; diff --git a/packages/agent-core-v2/src/agent/permissionGate/permissionGateService.ts b/packages/agent-core-v2/src/agent/permissionGate/permissionGateService.ts index d70c4fe30..c04b46fac 100644 --- a/packages/agent-core-v2/src/agent/permissionGate/permissionGateService.ts +++ b/packages/agent-core-v2/src/agent/permissionGate/permissionGateService.ts @@ -1,33 +1,48 @@ +import { InstantiationType } from '#/_base/di/extensions'; +import { IInstantiationService } from "#/_base/di/instantiation"; +import { Disposable } from "#/_base/di/lifecycle"; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; +import { abortable, isUserCancellation } from '#/_base/utils/abort'; +import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; import { IAgentPermissionPolicyService } from '#/agent/permissionPolicy/permissionPolicy'; import type { + ApprovalRequest, ApprovalResponse, PermissionData, PermissionPolicyResolution, PermissionPolicyResult, } from '#/agent/permissionPolicy/types'; -import { IInstantiationService } from "#/_base/di/instantiation"; -import { Disposable } from "#/_base/di/lifecycle"; -import { abortable, isUserCancellation } from '#/_base/utils/abort'; +import { IAgentPermissionRulesService } from '#/agent/permissionRules/permissionRules'; +import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; import type { AuthorizeToolExecutionResult, ResolvedToolExecutionHookContext, } from '#/agent/tool/toolHooks'; -import type { ToolInputDisplay } from '@moonshot-ai/protocol'; -import { ISessionApprovalService } from "#/session/approval/approval"; -import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; -import { IAgentPermissionRulesService } from '#/agent/permissionRules/permissionRules'; -import { ISessionContext } from '#/session/sessionContext/sessionContext'; -import { ITelemetryService } from '#/app/telemetry/telemetry'; import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor'; -import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; +import { IEventBus } from '#/app/event/eventBus'; +import { ITelemetryService } from '#/app/telemetry/telemetry'; +import { ISessionApprovalService } from "#/session/approval/approval"; +import { ISessionContext } from '#/session/sessionContext/sessionContext'; +import type { ToolInputDisplay } from '@moonshot-ai/protocol'; import { IAgentPermissionGate, - type PermissionApprovalRequestContext, - type PermissionApprovalResultContext, } from './permissionGate'; -import { InstantiationType } from '#/_base/di/extensions'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { IEventBus } from '#/app/event/eventBus'; + +export type PermissionApprovalRequestContext = ApprovalRequest & { + readonly sessionId?: string; + readonly agentId?: string; + readonly turnId: number; + readonly toolInput: unknown; +}; + +export type PermissionApprovalResultContext = PermissionApprovalRequestContext & + ( + | ApprovalResponse + | { + readonly decision: 'error'; + readonly error: string; + } + ); declare module '#/app/event/eventBus' { interface DomainEventMap { diff --git a/packages/agent-core-v2/src/agent/permissionMode/injection/permissionModeInjection.ts b/packages/agent-core-v2/src/agent/permissionMode/injection/permissionModeInjection.ts index 139d6a2c9..191fc4de3 100644 --- a/packages/agent-core-v2/src/agent/permissionMode/injection/permissionModeInjection.ts +++ b/packages/agent-core-v2/src/agent/permissionMode/injection/permissionModeInjection.ts @@ -1,27 +1,41 @@ -import type { PermissionMode } from '#/agent/permissionPolicy/types'; -import type { IDisposable } from "#/_base/di/lifecycle"; -import type { IAgentContextInjectorService } from '#/agent/contextInjector/contextInjector'; +/** + * `permissionMode` domain (L3) — permission-mode context injection. + * + * Owns the `permission_mode` context-injection provider. It reads the live mode + * from `IAgentPermissionModeService` and registers reminders through + * `contextInjector`. + */ + +import { Disposable } from '#/_base/di/lifecycle'; +import { IAgentContextInjectorService } from '#/agent/contextInjector/contextInjector'; import type { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; +import type { PermissionMode } from '#/agent/permissionPolicy/types'; import AUTO_MODE_ENTER_REMINDER from './permission-mode-auto-enter-reminder.md?raw'; import AUTO_MODE_EXIT_REMINDER from './permission-mode-auto-exit-reminder.md?raw'; const PERMISSION_MODE_INJECTION_VARIANT = 'permission_mode'; -export function registerPermissionModeInjection( - dynamicInjector: IAgentContextInjectorService, - permissionMode: Pick, -): IDisposable { - let lastMode: PermissionMode | undefined; - return dynamicInjector.register(PERMISSION_MODE_INJECTION_VARIANT, () => { - const previousMode = lastMode; - const currentMode = permissionMode.mode; +export class PermissionModeInjection extends Disposable { + private lastMode: PermissionMode | undefined; + + constructor( + private readonly permissionMode: Pick, + @IAgentContextInjectorService dynamicInjector: IAgentContextInjectorService, + ) { + super(); + this._register( + dynamicInjector.register(PERMISSION_MODE_INJECTION_VARIANT, () => this.reminder()), + ); + } + + private reminder(): string | undefined { + const previousMode = this.lastMode; + const currentMode = this.permissionMode.mode; if (currentMode === previousMode) return undefined; - lastMode = currentMode; + this.lastMode = currentMode; if (currentMode === 'auto') return AUTO_MODE_ENTER_REMINDER; if (previousMode === 'auto') return AUTO_MODE_EXIT_REMINDER; return undefined; - }); + } } - -export { registerPermissionModeInjection as PermissionModeInjection }; diff --git a/packages/agent-core-v2/src/agent/permissionMode/permissionMode.ts b/packages/agent-core-v2/src/agent/permissionMode/permissionMode.ts index 9574ad6ba..ea5cfe903 100644 --- a/packages/agent-core-v2/src/agent/permissionMode/permissionMode.ts +++ b/packages/agent-core-v2/src/agent/permissionMode/permissionMode.ts @@ -1,6 +1,5 @@ -import type { PermissionMode } from '#/agent/permissionPolicy/types'; import { createDecorator } from "#/_base/di/instantiation"; - +import type { PermissionMode } from '#/agent/permissionPolicy/types'; import type { Hooks } from '#/hooks'; export interface PermissionModeChangedContext { diff --git a/packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts b/packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts index db2320491..7df0a3963 100644 --- a/packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts +++ b/packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts @@ -5,17 +5,17 @@ * `PermissionModeModel`, mutating it only through the `permission.set_mode` Op * (`wire.dispatch(setMode({ mode }))`) and reading it through `wire.getModel`. * The `onChanged` hook is driven by a `wire.subscribe` on that model (firing - * only on actual changes), and the mode-aware reminder is registered through - * `contextInjector`. Bound at Agent scope. + * only on actual changes), and mode-aware reminders are registered through the + * permission-mode injection helper. Bound at Agent scope. */ import type { PermissionMode } from '#/agent/permissionPolicy/types'; +import { IInstantiationService } from '#/_base/di/instantiation'; import { Disposable } from '#/_base/di/lifecycle'; import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { IAgentContextInjectorService } from '#/agent/contextInjector/contextInjector'; import { OrderedHookSlot } from '#/hooks'; -import { registerPermissionModeInjection } from '#/agent/permissionMode/injection/permissionModeInjection'; +import { PermissionModeInjection } from '#/agent/permissionMode/injection/permissionModeInjection'; import { IAgentWireService } from '#/wire/tokens'; import type { IWireService } from '#/wire/wireService'; import { IAgentPermissionModeService } from './permissionMode'; @@ -33,7 +33,7 @@ export class AgentPermissionModeService extends Disposable implements IAgentPerm constructor( @IAgentWireService private readonly wire: IWireService, - @IAgentContextInjectorService dynamicInjector: IAgentContextInjectorService, + @IInstantiationService instantiation: IInstantiationService, ) { super(); this._register( @@ -42,7 +42,7 @@ export class AgentPermissionModeService extends Disposable implements IAgentPerm void this.hooks.onChanged.run({ mode, previousMode }); }), ); - this._register(registerPermissionModeInjection(dynamicInjector, this)); + this._register(instantiation.createInstance(PermissionModeInjection, this)); } get mode(): PermissionMode { diff --git a/packages/agent-core-v2/test/permissionMode/permissionMode.test.ts b/packages/agent-core-v2/test/permissionMode/permissionMode.test.ts index c8790ffc7..24c7afc09 100644 --- a/packages/agent-core-v2/test/permissionMode/permissionMode.test.ts +++ b/packages/agent-core-v2/test/permissionMode/permissionMode.test.ts @@ -3,7 +3,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SyncDescriptor } from '#/_base/di/descriptors'; import { DisposableStore } from '#/_base/di/lifecycle'; import { TestInstantiationService } from '#/_base/di/test'; -import { IAgentContextInjectorService } from '#/agent/contextInjector/contextInjector'; +import { + IAgentContextInjectorService, + type ContextInjectionProvider, +} from '#/agent/contextInjector/contextInjector'; import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; import { AgentPermissionModeService } from '#/agent/permissionMode/permissionModeService'; import { PermissionModeModel } from '#/agent/permissionMode/permissionModeOps'; @@ -19,9 +22,23 @@ import { WireService } from '#/wire/wireServiceImpl'; const SCOPE = 'wire'; const KEY = 'permission-mode-test'; +let registeredInjection: + | { + readonly name: string; + readonly provider: ContextInjectionProvider; + } + | undefined; + const injectorStub: IAgentContextInjectorService = { _serviceBrand: undefined, - register: () => ({ dispose: () => {} }), + register: (name, provider) => { + registeredInjection = { name, provider }; + return { + dispose: () => { + if (registeredInjection?.provider === provider) registeredInjection = undefined; + }, + }; + }, }; let disposables: DisposableStore; @@ -30,6 +47,7 @@ let log: IAppendLogStore; let svc: IAgentPermissionModeService; beforeEach(() => { + registeredInjection = undefined; disposables = new DisposableStore(); ix = disposables.add(new TestInstantiationService()); ix.stub(IFileSystemStorageService, new InMemoryStorageService()); @@ -51,6 +69,20 @@ async function readRecords(): Promise { return out; } +async function runRegisteredInjection(): Promise { + const provider = registeredInjection?.provider; + if (provider === undefined) throw new Error('expected permission mode injection provider'); + const content = await provider({ + injectedPositions: [], + lastInjectedAt: null, + isNewTurn: true, + }); + if (typeof content !== 'string' && content !== undefined) { + throw new Error('expected permission mode injection provider to return text'); + } + return content; +} + describe('AgentPermissionModeService (wire-backed)', () => { it('setMode updates mode and fires onChanged with mode/previousMode', () => { const changes: { mode: PermissionMode; previousMode: PermissionMode }[] = []; @@ -79,6 +111,19 @@ describe('AgentPermissionModeService (wire-backed)', () => { expect('payload' in records[0]!).toBe(false); }); + it('registers auto-mode reminder injection through the injection service', async () => { + expect(registeredInjection?.name).toBe('permission_mode'); + + expect(await runRegisteredInjection()).toBeUndefined(); + + svc.setMode('auto'); + expect(await runRegisteredInjection()).toContain('Auto permission mode is active'); + expect(await runRegisteredInjection()).toBeUndefined(); + + svc.setMode('manual'); + expect(await runRegisteredInjection()).toContain('Auto permission mode is no longer active'); + }); + it('replay rebuilds mode from a persisted record on a fresh WireService (silent)', async () => { const ix2 = disposables.add(new TestInstantiationService()); ix2.stub(IFileSystemStorageService, new InMemoryStorageService());