1import { assert, it } from "@effect/vitest";
2import {
3 CommandId,
4 EnvironmentId,
5 OrchestratorMcpFailure,
6 ProjectId,
7 ProviderDriverKind,
8 ProviderInstanceId,
9 ThreadId,
10} from "@t3tools/contracts";
11import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";
13import * as Ref from "effect/Ref";
14import * as Schema from "effect/Schema";
15import { McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
16
17import * as SqlitePersistence from "../persistence/Sqlite.ts";
18import { DispatchModeLimit } from "../orchestration-v2/DispatchModeLimit.ts";
19import { CodexProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/CodexAdapterV2.ts";
20import * as CommandReceiptStore from "../orchestration-v2/CommandReceiptStore.ts";
21import * as Orchestrator from "../orchestration-v2/Orchestrator.ts";
22import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";
23import * as ProviderAdapterRegistry from "../orchestration-v2/ProviderAdapterRegistry.ts";
24import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts";
25import * as ServerSettings from "../serverSettings.ts";
26import * as ProviderReplayHarness from "../orchestration-v2/testkit/ProviderReplayHarness.ts";
27import * as McpHttpServer from "./McpHttpServer.ts";
28import * as McpInvocationContext from "./McpInvocationContext.ts";
29import * as McpToolAccess from "./McpToolAccess.ts";
30import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
31
32const instanceId = ProviderInstanceId.make("codex");
33const adapter = {
34 instanceId,
35 driver: ProviderDriverKind.make("codex"),
36 getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),
37 planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),
38 openSession: () => Effect.die("No provider process needed for metadata commands"),
39} as ProviderAdapter.ProviderAdapterV2["Service"];
40const layerDatabase = SqlitePersistence.layerMemory;
41const layerOrchestrator = Layer.mergeAll(
42 layerDatabase,
43 ProjectionStore.layer.pipe(Layer.provide(layerDatabase)),
44 ProviderReplayHarness.layerWithRegistry(
45 { name: "mcp-mode-race" },
46 ProviderAdapterRegistry.layerFromAdapters([adapter]),
47 { databaseLayer: layerDatabase, runEffectWorker: false },
48 ),
49);
50
51const RenameTool = Tool.make("rename", {
52 parameters: Schema.Struct({ threadId: ThreadId }),
53 success: Schema.Struct({ renamed: Schema.Boolean }),
54 failure: OrchestratorMcpFailure,
55 failureMode: "return",
56 dependencies: [
57 McpInvocationContext.McpInvocationContext,
58 ThreadManagement.ThreadManagementService,
59 ],
60});
61const RenameToolkit = Toolkit.make(RenameTool);
62
63const supervisedClient: McpInvocationContext.McpInvocationScope = {
64 environmentId: EnvironmentId.make("environment"),
65 requestNamespace: "client:race",
66 thread: undefined,
67 client: { sessionId: "race", label: "Claude Code", access: "approval-required" },
68 capabilities: new Set(["orchestration"]),
69 issuedAt: 0,
70};
71
72const mcpClient = McpSchema.McpServerClient.of({
73 clientId: 1,
74 protocolVersion: "2025-06-18",
75 clientCapabilities: {},
76 clientInfo: { name: "mcp-race", version: "1" },
77 initializePayload: {
78 protocolVersion: "2025-06-18",
79 capabilities: {},
80 clientInfo: { name: "mcp-race", version: "1" },
81 },
82 getClient: Effect.die("unused"),
83});
84
85const decodeOutcome = Schema.decodeUnknownEffect(
86 Schema.fromJsonString(
87 Schema.Union([
88 Schema.Struct({ renamed: Schema.Boolean }),
89 Schema.Struct({ code: Schema.String }),
90 ]),
91 ),
92);
93
94const renameRacingTheUser = (threadId: ThreadId, userRaises: boolean) =>
95 Effect.gen(function* () {
96 const orchestrator = yield* Orchestrator.OrchestratorV2;
97 const threads = yield* ThreadManagement.ThreadManagementService;
98 const raised = yield* Ref.make(false);
99 const handlers = McpToolAccess.toLayer(RenameToolkit, {
100 rename: McpToolAccess.writesThreads(
101 (input) => [input.threadId],
102 (input) =>
103 Effect.gen(function* () {
104 if (userRaises) {
105 yield* orchestrator
106 .dispatch({
107 type: "thread.runtime-mode.set",
108 commandId: CommandId.make("user-raise"),
109 threadId: input.threadId,
110 runtimeMode: "full-access",
111 })
112 .pipe(Effect.provideService(DispatchModeLimit, undefined), Effect.orDie);
113 yield* Ref.set(raised, true);
114 }
115 yield* threads
116 .dispatch({
117 type: "thread.metadata.update",
118 commandId: CommandId.make(`agent-rename:${userRaises}`),
119 threadId: input.threadId,
120 title: "Renamed by the agent",
121 })
122 .pipe(
123 Effect.mapError(
124 () =>
125 new OrchestratorMcpFailure({
126 code: "orchestration_error",
127 message: "The rename failed.",
128 }),
129 ),
130 );
131 return { renamed: true };
132 }),
133 ),
134 });
135 const outcome = yield* McpServer.McpServer.pipe(
136 Effect.flatMap((server) => server.callTool({ name: "rename", arguments: { threadId } })),
137 Effect.flatMap(({ content }) =>
138 decodeOutcome(content.map((part) => (part.type === "text" ? part.text : "")).join("")),
139 ),
140 Effect.provideService(McpInvocationContext.McpInvocationContext, supervisedClient),
141 Effect.provideService(McpSchema.McpServerClient, mcpClient),
142 Effect.provide(
143 McpHttpServer.toolkitRegistration(RenameToolkit, handlers).pipe(
144 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
145 ),
146 ),
147 );
148 assert.equal(yield* Ref.get(raised), userRaises);
149 return outcome;
150 });
151
152const createSupervisedThread = (threadId: ThreadId) =>
153 Effect.gen(function* () {
154 const orchestrator = yield* Orchestrator.OrchestratorV2;
155 yield* orchestrator.dispatch({
156 type: "thread.create",
157 commandId: CommandId.make(`create:${threadId}`),
158 threadId,
159 projectId: ProjectId.make("project:mcp-race"),
160 title: "Before",
161 modelSelection: { instanceId, model: "gpt-5" },
162 runtimeMode: "approval-required",
163 interactionMode: "default",
164 branch: null,
165 worktreePath: null,
166 createdBy: "user",
167 creationSource: "web",
168 });
169 });
170
171it.layer(layerOrchestrator)("writesThreads against a mode raise", (it) => {
172 it.effect("refuses the write when the user raises the thread after the check", () =>
173 Effect.gen(function* () {
174 const projections = yield* ProjectionStore.ProjectionStoreV2;
175 const threadId = ThreadId.make("thread:race-raised");
176 yield* createSupervisedThread(threadId);
177 const outcome = yield* renameRacingTheUser(threadId, true);
178 assert.deepEqual(outcome, { code: "runtime_mode_escalation_denied" });
179 const shell = yield* projections.getThreadShell(threadId);
180 assert.equal(shell?.runtimeMode, "full-access");
181 assert.equal(shell?.title, "Before");
182 }).pipe(
183 Effect.provide(
184 ThreadManagement.layer.pipe(
185 Layer.provide(ServerSettings.layerTest()),
186 Layer.provide(CommandReceiptStore.layer.pipe(Layer.provide(layerDatabase))),
187 ),
188 ),
189 ),
190 );
191});