# Files changed · PR \#8728 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/8728/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(mcp\): update environment preferences](https://git.cafe/pingdotgg/t3code/pull/8728.md)

Version: 7

Head commit: aae1adc5097ced03fa716e4222db448e6425310f

Base commit: 512391db91759dc9f4a7b1e93ca3075fe1e90dea

Comparison base: 512391db91759dc9f4a7b1e93ca3075fe1e90dea

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/mcp/EnvironmentMcpService.test.ts

Status: modified; +15; −1

```diff
@@ -15,6 +15,8 @@
 import * as Schema from "effect/Schema";
 
 import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
+import { layer as threadCommandExecutorLayer } from "../orchestration-v2/ThreadCommandExecutor.ts";
+import { ThreadManagementService } from "../orchestration-v2/ThreadManagementService.ts";
 import * as ProviderRegistryModule from "../provider/Services/ProviderRegistry.ts";
 import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts";
 import * as ServerSettings from "../serverSettings.ts";
@@ -24,6 +26,10 @@
 const encodeEnvironmentMcpReadResult = Schema.encodeUnknownEffect(EnvironmentMcpReadResult);
 const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
 const environmentId = EnvironmentId.make("environment-test");
+const mutationDependencies = Layer.merge(
+  Layer.mock(ThreadManagementService)({}),
+  threadCommandExecutorLayer,
+);
 const scope: McpInvocationScope = {
   environmentId,
   threadId: ThreadId.make("thread-test"),
@@ -121,6 +127,7 @@
             followChangeRequestTemplates: true,
           },
         }),
+        mutationDependencies,
       ),
     ),
   );
@@ -211,6 +218,7 @@
               environmentLayer("Test", "1.0.0"),
               providerLayer,
               ServerSettings.layerTest({}),
+              mutationDependencies,
             ),
           ),
         ),
@@ -235,6 +243,7 @@
               environmentLayer("Test", "1.0.0"),
               unavailableRegistry,
               ServerSettings.layerTest(DEFAULT_SERVER_SETTINGS),
+              mutationDependencies,
             ),
           ),
         ),
@@ -270,7 +279,12 @@
       Effect.provide(
         EnvironmentMcp.layer.pipe(
           Layer.provide(
-            Layer.mergeAll(environmentLayer("Test", "1.0.0"), providerLayer, settingsLayer),
+            Layer.mergeAll(
+              environmentLayer("Test", "1.0.0"),
+              providerLayer,
+              settingsLayer,
+              mutationDependencies,
+            ),
           ),
         ),
       ),
```

## apps/server/src/mcp/EnvironmentMcpService.ts

Status: modified; +89; −4

```diff
@@ -4,10 +4,13 @@
   ENVIRONMENT_MCP_MAX_WRITING_INSTRUCTIONS,
   EnvironmentMcpFailure,
   type EnvironmentMcpPreferences,
+  type EnvironmentMcpPreferencesUpdateInput,
+  type EnvironmentMcpPreferencesUpdateResult,
   type EnvironmentMcpReadInput,
   type EnvironmentMcpReadResult,
   type ServerProvider,
   type ServerSettings,
+  type ServerSettingsPatch,
 } from "@t3tools/contracts";
 import * as Cause from "effect/Cause";
 import * as Context from "effect/Context";
@@ -15,6 +18,8 @@
 import * as Layer from "effect/Layer";
 
 import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
+import { ThreadCommandExecutor } from "../orchestration-v2/ThreadCommandExecutor.ts";
+import { ThreadManagementService } from "../orchestration-v2/ThreadManagementService.ts";
 import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts";
 import * as ServerSettingsModule from "../serverSettings.ts";
 import type { McpInvocationScope } from "./McpInvocationContext.ts";
@@ -26,6 +31,10 @@
       scope: McpInvocationScope,
       input: EnvironmentMcpReadInput,
     ) => Effect.Effect<EnvironmentMcpReadResult, EnvironmentMcpFailure>;
+    readonly updatePreferences: (
+      scope: McpInvocationScope,
+      input: EnvironmentMcpPreferencesUpdateInput,
+    ) => Effect.Effect<EnvironmentMcpPreferencesUpdateResult, EnvironmentMcpFailure>;
   }
 >()("t3/mcp/EnvironmentMcpService") {}
 
@@ -109,17 +118,73 @@
   } as const;
 };
 
-const make = Effect.gen(function* () {
+export const make = Effect.gen(function* () {
   const environment = yield* ServerEnvironment.ServerEnvironment;
   const providerRegistry = yield* ProviderRegistry;
   const settingsService = yield* ServerSettingsModule.ServerSettingsService;
+  const threads = yield* ThreadManagementService;
+  const threadDispatch = yield* ThreadCommandExecutor;
+
+  const requireCapability = (scope: McpInvocationScope) =>
+    scope.capabilities.has("orchestration")
+      ? Effect.void
+      : Effect.fail(new EnvironmentMcpFailure({ code: "capability_denied" }));
 
+  const requireCurrentEnvironment = (scope: McpInvocationScope) =>
+    unavailable(environment.getEnvironmentId, "environment_unavailable").pipe(
+      Effect.filterOrFail(
+        (currentEnvironmentId) => currentEnvironmentId === scope.environmentId,
+        () => new EnvironmentMcpFailure({ code: "environment_mismatch" }),
+      ),
+    );
+
+  const loadCurrentCaller = (scope: McpInvocationScope) =>
+    threads.getThreadShell(scope.threadId).pipe(
+      Effect.mapError(() => new EnvironmentMcpFailure({ code: "operation_failed" })),
+      Effect.flatMap((shell) =>
+        shell === null || shell.deletedAt !== null
+          ? Effect.fail(new EnvironmentMcpFailure({ code: "thread_not_found" }))
+          : Effect.succeed(shell),
+      ),
+    );
+
+  const preferencePatch = (input: EnvironmentMcpPreferencesUpdateInput): ServerSettingsPatch => ({
+    ...(input.defaultThreadEnvMode === undefined
+      ? {}
+      : { defaultThreadEnvMode: input.defaultThreadEnvMode }),
+    ...(input.newWorktreesStartFromOrigin === undefined
+      ? {}
+      : { newWorktreesStartFromOrigin: input.newWorktreesStartFromOrigin }),
+    ...(input.enableProviderUpdateChecks === undefined
+      ? {}
+      : { enableProviderUpdateChecks: input.enableProviderUpdateChecks }),
+    ...(input.backgroundActivity === undefined
+      ? {}
+      : { backgroundActivity: { profile: input.backgroundActivity.profile } }),
+    ...(input.sourceControlWritingStyle === undefined
+      ? {}
+      : {
+          sourceControlWritingStyle: {
+            ...(input.sourceControlWritingStyle.mode === undefined
+              ? {}
+              : { mode: input.sourceControlWritingStyle.mode }),
+            ...(input.sourceControlWritingStyle.customInstructions === undefined
+              ? {}
+              : { customInstructions: input.sourceControlWritingStyle.customInstructions }),
+            ...(input.sourceControlWritingStyle.followChangeRequestTemplates === undefined
+              ? {}
+              : {
+                  followChangeRequestTemplates:
+                    input.sourceControlWritingStyle.followChangeRequestTemplates,
+                }),
+          },
+        }),
+  });
+
   return EnvironmentMcpService.of({
     read: (scope, input) =>
       Effect.gen(function* () {
-        if (!scope.capabilities.has("orchestration")) {
-          return yield* new EnvironmentMcpFailure({ code: "capability_denied" });
+        yield* requireCapability(scope);
-        }
         const descriptor = yield* unavailable(environment.getDescriptor, "environment_unavailable");
         if (descriptor.environmentId !== scope.environmentId) {
           return yield* new EnvironmentMcpFailure({ code: "environment_mismatch" });
@@ -189,6 +254,26 @@
           preferenceScope: "server_owned",
         };
       }),
+    updatePreferences: (scope, input) =>
+      requireCapability(scope).pipe(
+        Effect.andThen(requireCurrentEnvironment(scope)),
+        Effect.andThen(
+          threadDispatch.withLock(
+            scope.threadId,
+            Effect.gen(function* () {
+              const caller = yield* loadCurrentCaller(scope);
+              if (caller.runtimeMode !== "full-access" || caller.interactionMode !== "default") {
+                return yield* new EnvironmentMcpFailure({ code: "permission_denied" });
+              }
+              const settings = yield* unavailable(
+                settingsService.updateSettings(preferencePatch(input)),
+                "settings_unavailable",
+              );
+              return { preferences: presentEnvironmentPreferences(settings) };
+            }),
+          ),
+        ),
+      ),
   });
 });
 
```

## apps/server/src/mcp/EnvironmentPreferencesMcpService.test.ts

Status: added; +359; −0

```diff
@@ -1,0 +1,359 @@
+import { assert, expect, it } from "@effect/vitest";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import {
+  CommandId,
+  DEFAULT_SERVER_SETTINGS,
+  EnvironmentId,
+  ENVIRONMENT_MCP_MAX_WRITING_INSTRUCTIONS,
+  EnvironmentMcpPreferencesUpdateInput,
+  ProjectId,
+  ProviderInstanceId,
+  ServerSettings,
+  ThreadId,
+  type OrchestrationV2ThreadShell,
+} from "@t3tools/contracts";
+import { fromLenientJson } from "@t3tools/shared/schemaJson";
+import * as DateTime from "effect/DateTime";
+import * as Deferred from "effect/Deferred";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import * as FileSystem from "effect/FileSystem";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as Ref from "effect/Ref";
+import * as Schema from "effect/Schema";
+import * as Stream from "effect/Stream";
+
+import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
+import * as ServerConfig from "../config.ts";
+import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
+import {
+  type KeyedSerialExecutor,
+  makeKeyedSerialExecutor,
+} from "../orchestration-v2/KeyedSerialExecutor.ts";
+import { OrchestratorV2 } from "../orchestration-v2/Orchestrator.ts";
+import {
+  ThreadCommandExecutor,
+  layer as threadCommandExecutorLayer,
+} from "../orchestration-v2/ThreadCommandExecutor.ts";
+import { makeLayer as makeProviderAdapterRegistryLayer } from "../orchestration-v2/ProviderAdapterRegistry.ts";
+import {
+  layer as threadManagementLayer,
+  ThreadManagementService,
+} from "../orchestration-v2/ThreadManagementService.ts";
+import { makeOrchestratorV2ReplayLayerWithRegistry } from "../orchestration-v2/testkit/ProviderReplayHarness.ts";
+import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
+import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts";
+import { makeProviderRegistryMock } from "../provider/testUtils/providerRegistryMock.ts";
+import * as ServerSettingsModule from "../serverSettings.ts";
+import {
+  EnvironmentMcpService,
+  layer as environmentMcpLayer,
+  make as makeEnvironmentMcpService,
+} from "./EnvironmentMcpService.ts";
+import type { McpInvocationScope } from "./McpInvocationContext.ts";
+
+const decodePreferencesUpdate = Schema.decodeUnknownEffect(EnvironmentMcpPreferencesUpdateInput);
+const decodePersistedSettings = Schema.decodeUnknownEffect(fromLenientJson(ServerSettings));
+const environmentId = EnvironmentId.make("environment-preferences-test");
+const threadId = ThreadId.make("thread:environment-preferences-test");
+const projectId = ProjectId.make("project:environment-preferences-test");
+const scope: McpInvocationScope = {
+  environmentId,
+  threadId,
+  providerSessionId: "provider-session:environment-preferences-test",
+  providerInstanceId: ProviderInstanceId.make("codex"),
+  capabilities: new Set(["orchestration"]),
+  issuedAt: 1,
+};
+
+const environmentLayer = Layer.succeed(
+  ServerEnvironment.ServerEnvironment,
+  ServerEnvironment.ServerEnvironment.of({
+    getEnvironmentId: Effect.succeed(environmentId),
+    getDescriptor: Effect.die("descriptor must not be read by preference updates"),
+  }),
+);
+
+const now = DateTime.makeUnsafe("2026-08-29T00:00:00.000Z");
+const fullAccessShell = {
+  createdBy: "user",
+  creationSource: "web",
+  id: threadId,
+  projectId,
+  title: "Environment preference caller",
+  providerInstanceId: ProviderInstanceId.make("codex"),
+  modelSelection: {
+    instanceId: ProviderInstanceId.make("codex"),
+    model: "gpt-5.6",
+  },
+  runtimeMode: "full-access",
+  interactionMode: "default",
+  branch: null,
+  worktreePath: null,
+  lineage: {
+    parentThreadId: null,
+    relationshipToParent: null,
+    rootThreadId: threadId,
+  },
+  forkedFrom: null,
+  activeProviderThreadId: null,
+  latestRunId: null,
+  activeRunId: null,
+  status: "idle",
+  pendingRuntimeRequest: null,
+  latestVisibleMessage: null,
+  latestUserMessageAt: null,
+  hasActionableProposedPlan: false,
+  itemCount: 0,
+  visibleItemCount: 0,
+  createdAt: now,
+  updatedAt: now,
+  archivedAt: null,
+  settledOverride: null,
+  settledAt: null,
+  deletedAt: null,
+} satisfies OrchestrationV2ThreadShell;
+
+const threadLayer = Layer.mock(ThreadManagementService)({
+  getThreadShell: () => Effect.succeed(fullAccessShell),
+});
+
+const makeServerSettingsLayer = () =>
+  ServerSettingsModule.layer.pipe(
+    Layer.provide(ServerSecretStore.layer),
+    Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)),
+    Layer.provideMerge(
+      Layer.fresh(
+        ServerConfig.layerTest(process.cwd(), {
+          prefix: "t3code-environment-preferences-test-",
+        }),
+      ),
+    ),
+  );
+
+it.effect("persists only allowlisted preferences and publishes the normalized settings", () =>
+  Effect.scoped(
+    Effect.gen(function* () {
+      const service = yield* EnvironmentMcpService;
+      const settingsService = yield* ServerSettingsModule.ServerSettingsService;
+      const serverConfig = yield* ServerConfig.ServerConfig;
+      const fileSystem = yield* FileSystem.FileSystem;
+      yield* settingsService.updateSettings({
+        enableProviderUpdateChecks: false,
+        sourceControlWritingStyle: {
+          mode: "custom",
+          customInstructions: "Keep this seeded instruction unless it is explicitly cleared.",
+          followChangeRequestTemplates: true,
+        },
+      });
+      const changes = yield* settingsService.subscribeChanges;
+
+      const result = yield* service.updatePreferences(scope, {
+        defaultThreadEnvMode: "worktree",
+        newWorktreesStartFromOrigin: false,
+        backgroundActivity: { profile: "performance" },
+        sourceControlWritingStyle: {
+          mode: "custom",
+          customInstructions: "",
+          followChangeRequestTemplates: false,
+        },
+      });
+      const published = Option.getOrThrow(yield* Stream.runHead(changes));
+      const readBack = yield* settingsService.getSettings;
+      const persistedRaw = yield* fileSystem.readFileString(serverConfig.settingsPath);
+      const persisted = yield* decodePersistedSettings(persistedRaw);
+
+      expect(result.preferences).toMatchObject({
+        defaultThreadEnvMode: "worktree",
+        newWorktreesStartFromOrigin: false,
+        enableProviderUpdateChecks: false,
+        backgroundActivity: { profile: "performance", baseProfile: null },
+        sourceControlWritingStyle: {
+          mode: "custom",
+          customInstructions: {
+            text: "",
+            characters: 0,
+            maximumCharacters: ENVIRONMENT_MCP_MAX_WRITING_INSTRUCTIONS,
+            truncated: false,
+          },
+          followChangeRequestTemplates: false,
+        },
+      });
+      expect(readBack.defaultThreadEnvMode).toBe("worktree");
+      expect(readBack.enableProviderUpdateChecks).toBe(false);
+      expect(readBack.enableAgentBrowserAccess).toBe(true);
+      expect(readBack.backgroundActivity).toEqual({
+        schemaVersion: 1,
+        profile: "performance",
+        overrides: {},
+      });
+      expect(published.sourceControlWritingStyle.customInstructions).toBe("");
+      expect(persisted.defaultThreadEnvMode).toBe("worktree");
+      expect(persisted.enableProviderUpdateChecks).toBe(false);
+      expect(persisted.sourceControlWritingStyle.customInstructions).toBe("");
+    }),
+  ).pipe(
+    Effect.provide(
+      environmentMcpLayer.pipe(
+        Layer.provideMerge(makeServerSettingsLayer()),
+        Layer.provide(
+          Layer.mergeAll(
+            environmentLayer,
+            Layer.succeed(ProviderRegistry, makeProviderRegistryMock()),
+            threadLayer,
+            threadCommandExecutorLayer,
+          ),
+        ),
+        Layer.provideMerge(NodeServices.layer),
+      ),
+    ),
+  ),
+);
+
+it.effect("validates writing instructions by Unicode code points", () =>
+  Effect.gen(function* () {
+    const exact = "😀".repeat(ENVIRONMENT_MCP_MAX_WRITING_INSTRUCTIONS);
+    const accepted = yield* decodePreferencesUpdate({
+      sourceControlWritingStyle: { customInstructions: exact },
+    });
+    expect(accepted.sourceControlWritingStyle?.customInstructions).toBe(exact);
+
+    const rejected = yield* Effect.exit(
+      decodePreferencesUpdate({
+        sourceControlWritingStyle: { customInstructions: `${exact}😀` },
+      }),
+    );
+    expect(rejected._tag).toBe("Failure");
+  }),
+);
+
+it.effect("observes a real caller downgrade before preference persistence", () =>
+  Effect.scoped(
+    Effect.gen(function* () {
+      const baseDispatch = yield* makeKeyedSerialExecutor<ThreadId>();
+      const raceActive = yield* Ref.make(false);
+      const acquisitionCount = yield* Ref.make(0);
+      const writerHolding = yield* Deferred.make<void>();
+      const updateWaiting = yield* Deferred.make<void>();
+      const releaseWriter = yield* Deferred.make<void>();
+      const instrumentedDispatch: KeyedSerialExecutor<ThreadId> = {
+        withLock: (requestedThreadId, effect) =>
+          Effect.gen(function* () {
+            if (!(yield* Ref.get(raceActive)) || requestedThreadId !== threadId) {
+              return yield* baseDispatch.withLock(requestedThreadId, effect);
+            }
+            const count = yield* Ref.updateAndGet(acquisitionCount, (value) => value + 1);
+            if (count === 1) {
+              return yield* baseDispatch.withLock(
+                requestedThreadId,
+                Deferred.succeed(writerHolding, undefined).pipe(
+                  Effect.andThen(Deferred.await(releaseWriter)),
+                  Effect.andThen(effect),
+                ),
+              );
+            }
+            yield* Deferred.succeed(updateWaiting, undefined);
+            return yield* baseDispatch.withLock(requestedThreadId, effect);
+          }),
+      };
+      const dispatchLayer = Layer.succeed(ThreadCommandExecutor, instrumentedDispatch);
+      const orchestratorLayer = makeOrchestratorV2ReplayLayerWithRegistry(
+        {
+          name: "environment-preferences-policy-race",
+          runtimePolicyOverride: {
+            cwd: "/tmp/environment-preferences-policy-race",
+            approvalPolicy: "never",
+            sandboxPolicy: {
+              type: "readOnly",
+              access: { type: "fullAccess" },
+              networkAccess: false,
+            },
+          },
+        },
+        makeProviderAdapterRegistryLayer([]),
+        { runEffectWorker: false, threadCommandExecutorLayer: dispatchLayer },
+      );
+      const applicationLayer = Layer.merge(
+        orchestratorLayer,
+        threadManagementLayer.pipe(Layer.provide(orchestratorLayer)),
+      );
+
+      yield* Effect.gen(function* () {
+        const threads = yield* ThreadManagementService;
+        const sharedDispatch = yield* ThreadCommandExecutor;
+        const orchestrator = yield* OrchestratorV2;
+        yield* threads.dispatch({
+          type: "thread.create",
+          createdBy: "user",
+          creationSource: "web",
+          commandId: CommandId.make("command:environment-preferences:create"),
+          threadId,
+          projectId,
+          title: "Environment preference caller",
+          modelSelection: {
+            instanceId: ProviderInstanceId.make("codex"),
+            model: "gpt-5.6",
+          },
+          runtimeMode: "full-access",
+          interactionMode: "default",
+          branch: null,
+          worktreePath: null,
+        });
+
+        let updateCalls = 0;
+        const settingsService = ServerSettingsModule.ServerSettingsService.of({
+          start: Effect.void,
+          ready: Effect.void,
+          getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS),
+          updateSettings: (_patch) =>
+            Effect.sync(() => {
+              updateCalls += 1;
+              return DEFAULT_SERVER_SETTINGS;
+            }),
+          streamChanges: Stream.empty,
+          subscribeChanges: Effect.succeed(Stream.empty),
+        });
+        const service = yield* makeEnvironmentMcpService.pipe(
+          Effect.provideService(ThreadManagementService, threads),
+          Effect.provideService(ThreadCommandExecutor, sharedDispatch),
+          Effect.provideService(
+            ServerEnvironment.ServerEnvironment,
+            ServerEnvironment.ServerEnvironment.of({
+              getEnvironmentId: Effect.succeed(environmentId),
+              getDescriptor: Effect.die("descriptor must not be read"),
+            }),
+          ),
+          Effect.provideService(ProviderRegistry, makeProviderRegistryMock()),
+          Effect.provideService(ServerSettingsModule.ServerSettingsService, settingsService),
+        );
+
+        yield* Ref.set(raceActive, true);
+        const downgrade = yield* threads
+          .dispatch({
+            type: "thread.interaction-mode.set",
+            commandId: CommandId.make("command:environment-preferences:downgrade"),
+            threadId,
+            interactionMode: "plan",
+          })
+          .pipe(Effect.forkScoped);
+        yield* Deferred.await(writerHolding);
+
+        const update = yield* service
+          .updatePreferences(scope, { enableProviderUpdateChecks: false })
+          .pipe(Effect.forkScoped);
+        yield* Deferred.await(updateWaiting);
+        yield* Deferred.succeed(releaseWriter, undefined);
+        yield* Fiber.join(downgrade);
+        const denied = yield* Fiber.join(update).pipe(Effect.flip);
+
+        assert.equal(denied.code, "permission_denied");
+        assert.equal(updateCalls, 0);
+        assert.equal(
+          (yield* orchestrator.getThreadProjection(threadId)).thread.interactionMode,
+          "plan",
+        );
+      }).pipe(Effect.provide(applicationLayer));
+    }),
+  ),
+);
```

## apps/server/src/mcp/toolkits/environment/handlers.ts

Status: modified; +6; −0

```diff
@@ -11,6 +11,12 @@
       const service = yield* EnvironmentMcpService;
       return yield* service.read(scope, input);
     }),
+  t3_environment_preferences_update: (input) =>
+    Effect.gen(function* () {
+      const scope = yield* McpInvocationContext;
+      const service = yield* EnvironmentMcpService;
+      return yield* service.updatePreferences(scope, input);
+    }),
 } satisfies Parameters<typeof EnvironmentToolkit.toLayer>[0];
 
 export const EnvironmentToolkitHandlersLive = EnvironmentToolkit.toLayer(handlers);
```

## apps/server/src/mcp/toolkits/environment/registration.test.ts

Status: modified; +19; −0

```diff
@@ -9,6 +9,7 @@
 
 import * as ServerEnvironment from "../../../environment/ServerEnvironment.ts";
 import * as GitWorkflowService from "../../../git/GitWorkflowService.ts";
+import { layer as threadCommandExecutorLayer } from "../../../orchestration-v2/ThreadCommandExecutor.ts";
 import { ThreadManagementService } from "../../../orchestration-v2/ThreadManagementService.ts";
 import * as ProjectService from "../../../project/ProjectService.ts";
 import * as ProjectSetupScriptRunner from "../../../project/ProjectSetupScriptRunner.ts";
@@ -23,6 +24,7 @@
 const environmentId = EnvironmentId.make("environment-scratch");
 const StubServicesLive = Layer.mergeAll(
   Layer.mock(ThreadManagementService)({}),
+  threadCommandExecutorLayer,
   Layer.mock(ProviderRegistry)({ getProviders: Effect.succeed([]) }),
   Layer.mock(ScheduledTaskService)({}),
   Layer.mock(ProjectService.ProjectService)({}),
@@ -133,6 +135,23 @@
         destructiveHint: false,
         openWorldHint: false,
       });
+      const preferencesUpdate = payload.result.tools.find(
+        (tool) => tool.name === "t3_environment_preferences_update",
+      );
+      expect(preferencesUpdate).toBeDefined();
+      expect(preferencesUpdate?.inputSchema.type).toBe("object");
+      expect(Object.keys(preferencesUpdate?.inputSchema.properties ?? {}).sort()).toEqual([
+        "backgroundActivity",
+        "defaultThreadEnvMode",
+        "enableProviderUpdateChecks",
+        "newWorktreesStartFromOrigin",
+        "sourceControlWritingStyle",
+      ]);
+      expect(preferencesUpdate?.annotations).toMatchObject({
+        readOnlyHint: false,
+        destructiveHint: true,
+        openWorldHint: false,
+      });
     }),
   ).pipe(Effect.provide(Layer.mergeAll(NodeHttpServer.layerTest, NodeServices.layer))),
 );
```

## apps/server/src/mcp/toolkits/environment/tools.ts

Status: modified; +21; −1

```diff
@@ -1,5 +1,7 @@
 import {
   EnvironmentMcpFailure,
+  EnvironmentMcpPreferencesUpdateInput,
+  EnvironmentMcpPreferencesUpdateResult,
   EnvironmentMcpReadInput,
   EnvironmentMcpReadResult,
 } from "@t3tools/contracts";
@@ -25,4 +27,22 @@
   .annotate(Tool.Idempotent, true)
   .annotate(Tool.OpenWorld, false);
 
+export const EnvironmentPreferencesUpdateTool = Tool.make("t3_environment_preferences_update", {
+  description:
+    "Update only the explicit server-owned preference fields in this input, then return the actual normalized preference allowlist after persistence. Omitted fields are unchanged; an explicit empty source-control customInstructions string clears it. This environment-wide mutation requires the calling thread to remain in full-access runtime mode and default interaction mode through persistence. It cannot change provider configuration, credentials, paths, observability, browser access, themes, pairing, tunnels, admin controls, client-local settings, or arbitrary server settings.",
+  parameters: EnvironmentMcpPreferencesUpdateInput,
+  success: EnvironmentMcpPreferencesUpdateResult,
+  failure: EnvironmentMcpFailure,
+  failureMode: "return",
-export const EnvironmentToolkit = Toolkit.make(EnvironmentReadTool);
+  dependencies,
+})
+  .annotate(Tool.Title, "Update T3 environment preferences")
+  .annotate(Tool.Readonly, false)
+  .annotate(Tool.Destructive, true)
+  .annotate(Tool.Idempotent, true)
+  .annotate(Tool.OpenWorld, false);
+
+export const EnvironmentToolkit = Toolkit.make(
+  EnvironmentReadTool,
+  EnvironmentPreferencesUpdateTool,
+);
```

## apps/server/src/mcp/toolkits/worktree/registration.test.ts

Status: modified; +2; −0

```diff
@@ -9,6 +9,7 @@
 
 import * as ServerEnvironment from "../../../environment/ServerEnvironment.ts";
 import * as GitWorkflowService from "../../../git/GitWorkflowService.ts";
+import { layer as threadCommandExecutorLayer } from "../../../orchestration-v2/ThreadCommandExecutor.ts";
 import { ThreadManagementService } from "../../../orchestration-v2/ThreadManagementService.ts";
 import * as ProjectService from "../../../project/ProjectService.ts";
 import * as ProjectSetupScriptRunner from "../../../project/ProjectSetupScriptRunner.ts";
@@ -29,6 +30,7 @@
   Layer.mock(GitWorkflowService.GitWorkflowService)({}),
   Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({}),
   Layer.mock(VcsStatusBroadcaster)({}),
+  threadCommandExecutorLayer,
 );
 
 const ToolsListPayload = Schema.fromJsonString(
```

## apps/server/src/orchestration-v2/Orchestrator.ts

Status: modified; +5; −4

```diff
@@ -7406,7 +7406,7 @@
   });
 });
 
-export const layer: Layer.Layer<
+export const layerFromThreadCommandExecutor: Layer.Layer<
   OrchestratorV2,
   never,
   | CheckpointServiceV2
@@ -7415,15 +7415,16 @@
   | ContextHandoffServiceV2
   | EventSinkV2
   | IdAllocatorV2
+  | ThreadCommandExecutor
   | ProviderAdapterRegistryV2
   | ProviderSessionManagerV2
   | ProviderSwitchServiceV2
   | ProjectionStoreV2
   | RuntimePolicyV2
   | ThreadForkServiceV2
+> = Layer.effect(OrchestratorV2, makeOrchestrator());
+
-> = Layer.effect(OrchestratorV2, makeOrchestrator()).pipe(
-  Layer.provide(threadCommandExecutorLayer),
-);
+export const layer = layerFromThreadCommandExecutor.pipe(Layer.provide(threadCommandExecutorLayer));
 
 export const layerUnavailable: Layer.Layer<OrchestratorV2> = Layer.succeed(
   OrchestratorV2,
```

## apps/server/src/orchestration-v2/runtimeLayer.ts

Status: modified; +3; −0

```diff
@@ -23,6 +23,7 @@
 import { layerFromOrchestrationEventStore as eventStoreLayer } from "./EventStore.ts";
 import { layer as idAllocatorLayer } from "./IdAllocator.ts";
 import { layer as legacyV1ThreadImporterLayer } from "./LegacyV1ThreadImporter.ts";
+import { layer as threadCommandExecutorLayer } from "./ThreadCommandExecutor.ts";
 import { layer as orchestratorLayer } from "./Orchestrator.ts";
 import { layer as projectionStoreLayer } from "./ProjectionStore.ts";
 import { layer as projectionMaintenanceLayer } from "./ProjectionMaintenance.ts";
@@ -194,6 +195,7 @@
       providerSwitchServiceProvided,
       runExecutionServiceProvided,
       threadForkServiceLayer,
+      threadCommandExecutorLayer,
     ),
   ),
 );
@@ -261,6 +263,7 @@
 );
 
 export const OrchestrationV2LayerLive = Layer.mergeAll(
+  threadCommandExecutorLayer,
   orchestratorProvided,
   threadManagementProvided,
   effectWorkerProvided,
```

## apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts

Status: modified; +33; −15

```diff
@@ -20,6 +20,10 @@
 import { layer as checkpointCaptureServiceLayer } from "../CheckpointCaptureService.ts";
 import { layer as checkpointServiceLayer } from "../CheckpointService.ts";
 import { layer as checkpointRollbackServiceLayer } from "../CheckpointRollbackService.ts";
+import {
+  ThreadCommandExecutor,
+  layer as threadCommandExecutorLayer,
+} from "../ThreadCommandExecutor.ts";
 import { layer as commandPolicyLayer } from "../CommandPolicy.ts";
 import { layer as commandReceiptStoreLayer } from "../CommandReceiptStore.ts";
 import { layer as contextHandoffServiceLayer } from "../ContextHandoffService.ts";
@@ -32,7 +36,7 @@
 import { layerFromStores as eventSinkLayer } from "../EventSink.ts";
 import { layer as eventStoreLayer } from "../EventStore.ts";
 import { layer as idAllocatorLayer } from "../IdAllocator.ts";
+import { layerFromThreadCommandExecutor as orchestratorLayer } from "../Orchestrator.ts";
-import { layer as orchestratorLayer } from "../Orchestrator.ts";
 import { layer as projectionStoreLayer } from "../ProjectionStore.ts";
 import { OrchestratorV2, type OrchestratorV2Error } from "../Orchestrator.ts";
 import { ProviderAdapterRegistryV2 } from "../ProviderAdapterRegistry.ts";
@@ -239,8 +243,12 @@
     >;
     readonly enableLegacyTokenStreaming?: boolean;
     readonly runEffectWorker?: boolean;
+    readonly threadCommandExecutorLayer?: Layer.Layer<ThreadCommandExecutor>;
   } = {},
+): Layer.Layer<
-): Layer.Layer<OrchestratorV2, Error | MigrationError | PlatformError.PlatformError | SqlError> {
+  OrchestratorV2 | ThreadCommandExecutor,
+  Error | MigrationError | PlatformError.PlatformError | SqlError
+> {
   const serverConfigLayer = Layer.effect(
     ServerConfig,
     makeReplayServerConfig(scenario.name).pipe(Effect.orDie),
@@ -255,6 +263,7 @@
   const serverSettingsLayer = ServerSettingsService.layerTest({
     enableLegacyTokenStreaming: options.enableLegacyTokenStreaming ?? false,
   }).pipe(Layer.orDie);
+  const dispatchLockLayer = options.threadCommandExecutorLayer ?? threadCommandExecutorLayer;
   const storesLayer = Layer.mergeAll(
     eventStoreLayer,
     projectionStoreLayer,
@@ -329,6 +338,7 @@
         Layer.mock(ProviderAuthService)({ tryHandlePromptCommand: () => Effect.succeed(false) }),
         runExecutionServiceProvided,
         runtimeLayer,
+        dispatchLockLayer,
       ),
     ),
   );
@@ -393,26 +403,34 @@
         providerSwitchServiceProvided,
         runExecutionServiceProvided,
         threadForkServiceLayer,
+        dispatchLockLayer,
       ),
     ),
   );
-  const replayRuntime = Layer.merge(orchestratorProvided, effectWorkerProvided).pipe(
-    Layer.provide(worktreeRepairDependenciesTestLayer),
-    Layer.provide(NodeServices.layer),
+  const replayRuntime = Layer.mergeAll(
+    dispatchLockLayer,
-  );
+    orchestratorProvided,
+    effectWorkerProvided,
+  ).pipe(Layer.provide(worktreeRepairDependenciesTestLayer), Layer.provide(NodeServices.layer));
 
   // Build the daemon from the exact worker instance exposed alongside the
   // orchestrator. Keeping this acquisition in the replay layer makes the
   // outbox lifecycle explicit and prevents test-only command-side draining.
   if (options.runEffectWorker === false) {
+    return Layer.merge(
+      dispatchLockLayer,
-    return orchestratorProvided.pipe(Layer.provide(NodeServices.layer));
+      orchestratorProvided.pipe(Layer.provide(NodeServices.layer)),
+    );
   }
-  return Layer.effect(
-    OrchestratorV2,
-    Effect.gen(function* () {
-      const orchestrator = yield* OrchestratorV2;
-      yield* runEffectWorkerDaemon.pipe(Effect.forkScoped);
-      return orchestrator;
-    }),
+  return Layer.merge(
+    dispatchLockLayer,
-  ).pipe(Layer.provide(replayRuntime));
+    Layer.effect(
+      OrchestratorV2,
+      Effect.gen(function* () {
+        const orchestrator = yield* OrchestratorV2;
+        yield* runEffectWorkerDaemon.pipe(Effect.forkScoped);
+        return orchestrator;
+      }),
+    ).pipe(Layer.provide(replayRuntime)),
+  );
 }
```

## docs/internals/environment-mcp.md

Status: modified; +14; −0

```diff
@@ -22,3 +22,17 @@
 field; never spread a server settings or provider configuration object into an
 MCP result. Environment reads use cached provider state and do not call provider
 refresh or the usage service.
+
+`t3_environment_preferences_update` accepts a separate literal patch schema and
+constructs an existing `ServerSettingsPatch` field by field. The mutation runs
+under `ThreadDispatchLockV2` for the calling thread. It reloads the caller under
+that same lock, requires `full-access` plus `default`, and holds the lock through
+`ServerSettingsService.updateSettings`. Ordinary V2 policy writers use the same
+lock, so a concurrent downgrade is observed before persistence. The settings
+service remains responsible for normalization, durable storage, and existing
+cross-client notifications.
+
+The mutation exposes preset background profiles, not arbitrary timing
+overrides. Empty source-control instructions are an intentional clear; omitted
+fields remain unchanged. Its result is projected from the settings returned
+after persistence rather than from the request.
```

## docs/user/environment-mcp.md

Status: modified; +23; −0

```diff
@@ -20,3 +20,26 @@
 mobile clients connected to this environment. Client-local appearance and
 desktop preferences are not available through this tool. Reading the
 environment does not scan usage transcripts or trigger provider maintenance.
+
+`t3_environment_preferences_update` can change only these server-owned fields:
+
+- the default environment mode for new threads;
+- whether new worktrees start from the configured origin;
+- provider update checks;
+- one of the balanced, performance, or battery-saver background-activity
+  presets;
+- source control writing mode, custom instructions, and change-request template
+  behavior.
+
+Omitted fields are retained. An explicit empty custom-instructions string clears
+that value. The tool applies background activity through the existing nested
+profile normalization and returns the actual allowlisted settings after they
+are persisted. Existing settings notifications update connected web, desktop,
+and mobile clients.
+
+Environment-wide changes are available only while the calling thread remains
+in full-access runtime mode with default interaction mode. Plan,
+approval-required, and other restrictive callers cannot change these settings.
+The tool cannot change provider configuration or credentials, filesystem paths,
+observability, browser access, themes, pairing, tunnels, admin controls, or
+client-local preferences.
```

## packages/contracts/src/environmentMcp.ts

Status: modified; +70; −2

```diff
@@ -118,6 +118,68 @@
 });
 export type EnvironmentMcpPreferences = typeof EnvironmentMcpPreferences.Type;
 
+const EnvironmentMcpWritingInstructions = Schema.String.check(
+  Schema.makeFilter((value) =>
+    Array.from(value).length <= ENVIRONMENT_MCP_MAX_WRITING_INSTRUCTIONS
+      ? undefined
+      : `customInstructions must not exceed ${ENVIRONMENT_MCP_MAX_WRITING_INSTRUCTIONS} Unicode characters.`,
+  ),
+);
+
+export const EnvironmentMcpPreferencesUpdateInput = Schema.Struct({
+  defaultThreadEnvMode: Schema.optional(ThreadEnvMode).annotate({
+    description: "Default execution environment for newly created threads.",
+  }),
+  newWorktreesStartFromOrigin: Schema.optional(Schema.Boolean).annotate({
+    description: "Whether newly created worktrees start from the configured origin branch.",
+  }),
+  enableProviderUpdateChecks: Schema.optional(Schema.Boolean).annotate({
+    description: "Whether the server performs its existing provider update checks.",
+  }),
+  backgroundActivity: Schema.optional(
+    Schema.Struct({
+      profile: BackgroundActivityProfile.annotate({
+        description:
+          "Apply one supported background-activity preset through the existing settings normalization path.",
+      }),
+    }),
+  ),
+  sourceControlWritingStyle: Schema.optional(
+    Schema.Struct({
+      mode: Schema.optional(SourceControlWritingStyleMode),
+      customInstructions: Schema.optional(EnvironmentMcpWritingInstructions).annotate({
+        description: `Replacement custom instructions, bounded to ${ENVIRONMENT_MCP_MAX_WRITING_INSTRUCTIONS} Unicode characters. An empty string clears them.`,
+      }),
+      followChangeRequestTemplates: Schema.optional(Schema.Boolean),
+    }).check(
+      Schema.makeFilter((value) =>
+        value.mode !== undefined ||
+        value.customInstructions !== undefined ||
+        value.followChangeRequestTemplates !== undefined
+          ? undefined
+          : "sourceControlWritingStyle must include at least one field.",
+      ),
+    ),
+  ),
+}).check(
+  Schema.makeFilter((value) =>
+    value.defaultThreadEnvMode !== undefined ||
+    value.newWorktreesStartFromOrigin !== undefined ||
+    value.enableProviderUpdateChecks !== undefined ||
+    value.backgroundActivity !== undefined ||
+    value.sourceControlWritingStyle !== undefined
+      ? undefined
+      : "Provide at least one preference field.",
+  ),
+);
+export type EnvironmentMcpPreferencesUpdateInput = typeof EnvironmentMcpPreferencesUpdateInput.Type;
+
+export const EnvironmentMcpPreferencesUpdateResult = Schema.Struct({
+  preferences: EnvironmentMcpPreferences,
+});
+export type EnvironmentMcpPreferencesUpdateResult =
+  typeof EnvironmentMcpPreferencesUpdateResult.Type;
+
 export const EnvironmentMcpReadResult = Schema.Struct({
   identity: Schema.Struct({
     environmentId: EnvironmentId,
@@ -144,6 +206,8 @@
       "environment_mismatch",
       "provider_registry_unavailable",
       "settings_unavailable",
+      "thread_not_found",
+      "permission_denied",
       "operation_failed",
     ]),
   },
@@ -151,15 +215,19 @@
   override get message(): string {
     switch (this.code) {
       case "capability_denied":
+        return "This MCP credential does not grant environment capabilities.";
-        return "This MCP credential does not grant environment read capabilities.";
       case "environment_unavailable":
-        return "The current environment descriptor is unavailable.";
+        return "The current environment service is unavailable.";
       case "environment_mismatch":
         return "The MCP credential does not belong to the running environment.";
       case "provider_registry_unavailable":
         return "The provider registry is unavailable.";
       case "settings_unavailable":
         return "Server-owned preferences are unavailable.";
+      case "thread_not_found":
+        return "The calling thread was not found.";
+      case "permission_denied":
+        return "Environment preference updates require a full-access caller with default interaction mode.";
       case "operation_failed":
         return "The environment operation failed.";
     }
```

## packages/shared/src/t3McpToolPresentation.test.ts

Status: modified; +6; −0

```diff
@@ -58,6 +58,12 @@
       displayName: "Read current T3 environment",
       logo: "t3-code",
     });
+    expect(resolveT3McpToolPresentation("mcp__t3-code__t3_environment_preferences_update")).toEqual(
+      {
+        displayName: "Update T3 environment preferences",
+        logo: "t3-code",
+      },
+    );
   });
 
   it("keeps unknown MCP tools on the generic renderer path", () => {
```

## packages/shared/src/t3McpToolPresentation.ts

Status: modified; +1; −0

```diff
@@ -55,6 +55,7 @@
   t3_worktree_handoff: { displayName: "Hand off thread to a git worktree" },
   t3_worktree_status: { displayName: "Get thread worktree status" },
   t3_environment_read: { displayName: "Read current T3 environment" },
+  t3_environment_preferences_update: { displayName: "Update T3 environment preferences" },
   preview_status: { displayName: "Get preview browser status" },
   preview_open: { displayName: "Open a page in the preview browser" },
   preview_navigate: { displayName: "Navigate the preview browser" },
```
