Skip to content

Commit 4aa047f

Browse files
committed
unified encapsulation of the service creation logic after fork
1 parent e5b2165 commit 4aa047f

7 files changed

Lines changed: 156 additions & 192 deletions

File tree

packages/codingcode/src/approval/async-confirm.ts

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -35,47 +35,45 @@ export function hasEmitter(sessionId: string): boolean {
3535
}
3636

3737
export class ApprovalWaitService extends Effect.Service<ApprovalWaitService>()('ApprovalWait', {
38-
effect: Effect.gen(function* () {
39-
return {
40-
waitForConfirm: (id: string, sessionId: string): Effect.Effect<ConfirmResult> =>
41-
Effect.gen(function* () {
42-
const d = yield* Deferred.make<ConfirmResult, never>();
43-
pending.set(id, { deferred: d, sessionId });
44-
return yield* Deferred.await(d);
45-
}),
38+
effect: Effect.succeed({
39+
waitForConfirm: (id: string, sessionId: string): Effect.Effect<ConfirmResult> =>
40+
Effect.gen(function* () {
41+
const d = yield* Deferred.make<ConfirmResult, never>();
42+
pending.set(id, { deferred: d, sessionId });
43+
return yield* Deferred.await(d);
44+
}),
4645

47-
resolveConfirm: (
48-
id: string,
49-
_sessionId: string,
50-
result: ConfirmResult
51-
): Effect.Effect<boolean> =>
52-
Effect.sync(() => {
53-
const entry = pending.get(id);
54-
if (!entry) return false;
55-
pending.delete(id);
56-
Deferred.unsafeDone(entry.deferred, Effect.succeed(result));
57-
return true;
58-
}),
46+
resolveConfirm: (
47+
id: string,
48+
_sessionId: string,
49+
result: ConfirmResult
50+
): Effect.Effect<boolean> =>
51+
Effect.sync(() => {
52+
const entry = pending.get(id);
53+
if (!entry) return false;
54+
pending.delete(id);
55+
Deferred.unsafeDone(entry.deferred, Effect.succeed(result));
56+
return true;
57+
}),
5958

60-
getPending: (sessionId?: string): Effect.Effect<string[]> =>
61-
Effect.sync(() => {
62-
if (sessionId) {
63-
return Array.from(pending.entries())
64-
.filter(([_, e]) => e.sessionId === sessionId)
65-
.map(([id]) => id);
66-
}
67-
return Array.from(pending.keys());
68-
}),
59+
getPending: (sessionId?: string): Effect.Effect<string[]> =>
60+
Effect.sync(() => {
61+
if (sessionId) {
62+
return Array.from(pending.entries())
63+
.filter(([_, e]) => e.sessionId === sessionId)
64+
.map(([id]) => id);
65+
}
66+
return Array.from(pending.keys());
67+
}),
6968

70-
emitApprovalRequest: (
71-
sessionId: string,
72-
id: string,
73-
tool: string,
74-
args: Record<string, unknown>
75-
): Effect.Effect<void> =>
76-
Effect.sync(() => {
77-
emitters.get(sessionId)?.(id, tool, args);
78-
}),
79-
};
69+
emitApprovalRequest: (
70+
sessionId: string,
71+
id: string,
72+
tool: string,
73+
args: Record<string, unknown>
74+
): Effect.Effect<void> =>
75+
Effect.sync(() => {
76+
emitters.get(sessionId)?.(id, tool, args);
77+
}),
8078
}),
8179
}) {}

packages/codingcode/src/approval/index.ts

Lines changed: 84 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,84 @@ export class ApprovalService extends Effect.Service<ApprovalService>()('Approval
4242
};
4343
}
4444

45+
function makeForkedService(
46+
engine: RuleEngine,
47+
permMode: PermissionMode,
48+
roTools: Set<string>,
49+
destTools: Set<string>
50+
): ApprovalService {
51+
let currentPermMode = permMode;
52+
return ApprovalService.make({
53+
evaluate: (request: {
54+
tool: string;
55+
input: Record<string, unknown>;
56+
context?: Record<string, unknown>;
57+
callId?: string;
58+
sessionId: string;
59+
}): Effect.Effect<ApprovalDecision> =>
60+
Effect.gen(function* () {
61+
return yield* runPipeline(
62+
{
63+
tool: request.tool,
64+
input: request.input,
65+
context: request.context,
66+
callId: request.callId,
67+
},
68+
{
69+
ruleEngine: engine,
70+
readonlyTools: roTools,
71+
destructiveTools: destTools,
72+
permissionMode: currentPermMode,
73+
hooks: buildPipelineHooks(),
74+
interactive: process.stdin.isTTY ?? false,
75+
asyncConfirm: hasEmitter(request.sessionId),
76+
asyncConfirmService: approvalWait,
77+
onAlways: (rule) => engine.addRule(rule),
78+
onNever: (rule) => engine.addRule(rule),
79+
sessionId: request.sessionId,
80+
callId: request.callId,
81+
}
82+
);
83+
}),
84+
addRule: (rule: PermissionRule): Effect.Effect<void> =>
85+
Effect.sync(() => engine.addRule(rule)),
86+
removeRule: (id: string): Effect.Effect<void> => Effect.sync(() => engine.removeRule(id)),
87+
setPermissionMode: (mode: PermissionMode): Effect.Effect<void> =>
88+
Effect.sync(() => {
89+
currentPermMode = mode;
90+
}),
91+
getPermissionMode: (): PermissionMode => currentPermMode,
92+
fork: (opts?: {
93+
extraDenyRules?: PermissionRule[];
94+
readonly?: boolean;
95+
}): Effect.Effect<ApprovalService> =>
96+
Effect.sync(() => {
97+
const nextEngine = createRuleEngine(engine.getAllRules());
98+
if (opts?.extraDenyRules) {
99+
for (const rule of opts.extraDenyRules) {
100+
nextEngine.addRule(rule);
101+
}
102+
}
103+
if (opts?.readonly) {
104+
for (const toolName of DESTRUCTIVE_TOOL_NAMES) {
105+
nextEngine.addRule({
106+
id: `readonly-${toolName}`,
107+
action: 'deny' as const,
108+
toolPattern: toolName,
109+
source: 'system' as const,
110+
});
111+
}
112+
}
113+
return makeForkedService(
114+
nextEngine,
115+
currentPermMode,
116+
new Set(roTools),
117+
new Set(destTools)
118+
);
119+
}),
120+
});
121+
}
122+
45123
return {
46124
evaluate: (request: {
47125
tool: string;
@@ -110,116 +188,12 @@ export class ApprovalService extends Effect.Service<ApprovalService>()('Approval
110188
childEngine.addRule(rule);
111189
}
112190
}
113-
let childPermissionMode: PermissionMode = _globalPermissionMode;
114-
const childReadonlyTools = new Set(readonlyTools);
115-
const childDestructiveTools = new Set(destructiveTools);
116-
return {
117-
evaluate: (request: {
118-
tool: string;
119-
input: Record<string, unknown>;
120-
context?: Record<string, unknown>;
121-
callId?: string;
122-
sessionId: string;
123-
}) =>
124-
Effect.gen(function* () {
125-
return yield* runPipeline(
126-
{
127-
tool: request.tool,
128-
input: request.input,
129-
context: request.context,
130-
callId: request.callId,
131-
},
132-
{
133-
ruleEngine: childEngine,
134-
readonlyTools: childReadonlyTools,
135-
destructiveTools: childDestructiveTools,
136-
permissionMode: childPermissionMode,
137-
hooks: buildPipelineHooks(),
138-
interactive: process.stdin.isTTY ?? false,
139-
asyncConfirm: hasEmitter(request.sessionId),
140-
asyncConfirmService: approvalWait,
141-
onAlways: (rule) => childEngine.addRule(rule),
142-
onNever: (rule) => childEngine.addRule(rule),
143-
sessionId: request.sessionId,
144-
callId: request.callId,
145-
}
146-
);
147-
}),
148-
addRule: (rule: PermissionRule) => Effect.sync(() => childEngine.addRule(rule)),
149-
removeRule: (id: string) => Effect.sync(() => childEngine.removeRule(id)),
150-
setPermissionMode: (mode: PermissionMode) =>
151-
Effect.sync(() => {
152-
childPermissionMode = mode;
153-
}),
154-
getPermissionMode: () => childPermissionMode,
155-
fork: (opts2: any) => {
156-
const nestedParentRules = childEngine.getAllRules();
157-
const nestedEngine = createRuleEngine(nestedParentRules);
158-
if (opts2?.extraDenyRules) {
159-
for (const rule of opts2.extraDenyRules) {
160-
nestedEngine.addRule(rule);
161-
}
162-
}
163-
if (opts2?.readonly) {
164-
const denyRules: PermissionRule[] = DESTRUCTIVE_TOOL_NAMES.map((toolName) => ({
165-
id: `readonly-${toolName}`,
166-
action: 'deny' as const,
167-
toolPattern: toolName,
168-
source: 'system' as const,
169-
}));
170-
for (const rule of denyRules) {
171-
nestedEngine.addRule(rule);
172-
}
173-
}
174-
let nestedPermissionMode: PermissionMode = childPermissionMode;
175-
const nestedReadonlyTools = new Set(childReadonlyTools);
176-
const nestedDestructiveTools = new Set(childDestructiveTools);
177-
return Effect.succeed({
178-
evaluate: (request: {
179-
tool: string;
180-
input: Record<string, unknown>;
181-
context?: Record<string, unknown>;
182-
callId?: string;
183-
sessionId: string;
184-
}) =>
185-
Effect.gen(function* () {
186-
return yield* runPipeline(
187-
{
188-
tool: request.tool,
189-
input: request.input,
190-
context: request.context,
191-
callId: request.callId,
192-
},
193-
{
194-
ruleEngine: nestedEngine,
195-
readonlyTools: nestedReadonlyTools,
196-
destructiveTools: nestedDestructiveTools,
197-
permissionMode: nestedPermissionMode,
198-
hooks: buildPipelineHooks(),
199-
interactive: process.stdin.isTTY ?? false,
200-
asyncConfirm: hasEmitter(request.sessionId),
201-
asyncConfirmService: approvalWait,
202-
onAlways: (rule: PermissionRule) => nestedEngine.addRule(rule),
203-
onNever: (rule: PermissionRule) => nestedEngine.addRule(rule),
204-
sessionId: request.sessionId,
205-
callId: request.callId,
206-
}
207-
);
208-
}),
209-
addRule: (rule: PermissionRule) => Effect.sync(() => nestedEngine.addRule(rule)),
210-
removeRule: (id: string) => Effect.sync(() => nestedEngine.removeRule(id)),
211-
setPermissionMode: (mode: PermissionMode) =>
212-
Effect.sync(() => {
213-
nestedPermissionMode = mode;
214-
}),
215-
getPermissionMode: () => nestedPermissionMode,
216-
fork: (opts3: any) => {
217-
// Nested fork not commonly used, use parent's fork for now
218-
return Effect.fail(new Error('Deeply nested fork not supported'));
219-
},
220-
} as any);
221-
},
222-
} as any;
191+
return makeForkedService(
192+
childEngine,
193+
_globalPermissionMode,
194+
new Set(readonlyTools),
195+
new Set(destructiveTools)
196+
);
223197
}),
224198
};
225199
}),

packages/codingcode/src/approval/pipeline.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Effect } from 'effect';
22
import type { ApprovalDecision, PermissionMode, PermissionRule, ToolCallRequest } from './types';
33
import type { RuleEngine } from './rule-engine';
4-
import type { ConfirmResult } from './confirmation';
54
import { userConfirm, userConfirmAsync } from './confirmation';
65
import type { ApprovalWaitService } from './async-confirm';
76

packages/codingcode/src/client/direct.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { Effect } from 'effect';
22
import type { AgentEvent } from '../agent/agent.js';
33
import { sendMessage } from '../agent/agent.js';
4-
import { ApprovalWaitService } from '../approval/async-confirm.js';
5-
import { parseApprovalResponse } from '../approval/response.js';
64
import { AppLayer } from '../layer.js';
75
import { CheckpointService } from '../checkpoint/checkpoint-service.js';
8-
import { getLLMClient, switchModel as switchActiveModel } from '../llm/factory.js';
6+
import { getLLMClient } from '../llm/factory.js';
97
import { getWorkspaceCwd } from '../core/workspace.js';
108
import { getGlobalPermissionMode, setGlobalPermissionMode } from '../approval/index.js';
119
import type { PermissionMode } from '../approval/types.js';

packages/codingcode/src/client/direct/agent-runtime.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import { sendMessage } from '../../agent/agent.js';
33
import { ApprovalWaitService } from '../../approval/async-confirm.js';
44
import { parseApprovalResponse } from '../../approval/response.js';
55
import { ContextService } from '../../context/context.js';
6-
import { AppLayer } from '../../layer.js';
76
import type { StreamChunk } from '../types.js';
87
import { agentEventToStreamChunk } from '../direct.js';
9-
import { getWorkspaceCwd } from '../../core/workspace.js';
108

119
export interface AgentRuntimeClient {
1210
sendMessage(

packages/codingcode/src/client/direct/sessions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function createDirectSessionClient(
5555
runWithLayer: <T>(eff: any) => Promise<T>
5656
): SessionClient {
5757
return {
58-
async createSession({ cwd, initialPermissionMode }) {
58+
async createSession({ cwd, initialPermissionMode: _initialPermissionMode }) {
5959
return runWithLayer(
6060
Effect.gen(function* () {
6161
const svc = yield* SessionService;

0 commit comments

Comments
 (0)