refactor: align permission mode injection

This commit is contained in:
_Kerman 2026-07-07 22:10:26 +08:00
parent 22e6413ff1
commit 07f3f1cc36
6 changed files with 116 additions and 62 deletions

View file

@ -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;

View file

@ -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 {

View file

@ -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<IAgentPermissionModeService, 'mode'>,
): 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<IAgentPermissionModeService, 'mode'>,
@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 };

View file

@ -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 {

View file

@ -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 {

View file

@ -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<PersistedRecord[]> {
return out;
}
async function runRegisteredInjection(): Promise<string | undefined> {
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());