From b71b2b8ffcdedfea35da0bfe0a4c584f00d0d64f Mon Sep 17 00:00:00 2001 From: PR Batch Tester Date: Wed, 28 Oct 2026 12:18:09 +0000 Subject: [PATCH 1/4] fix(server): Swift TestFlight clients can read server settings again #26238 removed the server.getSettings RPC because nothing in the tree called it, but installed Swift TestFlight builds still request it on connect and from the composer, so they now fail with "Unknown request tag: server.getSettings". Restore the RPC with its original payload, result, errors, read scope and redacted handler. Fixes #26446 Co-Authored-By: Claude Opus 5.5 --- apps/server/src/auth/RpcAuthorization.ts | 1 + .../src/observability/RpcInstrumentation.ts | 1 + apps/server/src/ws.test.ts | 77 +++++++++++++++++++ apps/server/src/ws.ts | 2 + packages/contracts/src/rpc.ts | 9 +++ 5 files changed, 90 insertions(+) diff --git a/apps/server/src/auth/RpcAuthorization.ts b/apps/server/src/auth/RpcAuthorization.ts index f127f94fdba35..c479ea0627e7a 100644 --- a/apps/server/src/auth/RpcAuthorization.ts +++ b/apps/server/src/auth/RpcAuthorization.ts @@ -83,6 +83,7 @@ export const RPC_REQUIRED_SCOPES = { [WS_METHODS.serverRemoveKeybinding]: AuthSettingsWriteScope, [WS_METHODS.serverGetStorageCleanupReport]: AuthOrchestrationReadScope, [WS_METHODS.serverGetDatabaseStorage]: AuthOrchestrationReadScope, + [WS_METHODS.serverGetSettings]: AuthOrchestrationReadScope, [WS_METHODS.peerLinksList]: AuthAccessReadScope, [WS_METHODS.serverUpdateSettings]: AuthSettingsWriteScope, [WS_METHODS.serverSearchAcpRegistry]: AuthOrchestrationReadScope, diff --git a/apps/server/src/observability/RpcInstrumentation.ts b/apps/server/src/observability/RpcInstrumentation.ts index 504e533b37e05..6d12c5596a32a 100644 --- a/apps/server/src/observability/RpcInstrumentation.ts +++ b/apps/server/src/observability/RpcInstrumentation.ts @@ -60,6 +60,7 @@ const RPC_AGGREGATES = { [WS_METHODS.peerLinksLink]: "peerLinks", [WS_METHODS.peerLinksUnlink]: "peerLinks", [WS_METHODS.peerLinksAnswerRequest]: "peerLinks", + [WS_METHODS.serverGetSettings]: "server", [WS_METHODS.serverUpdateSettings]: "server", [WS_METHODS.serverSearchAcpRegistry]: "server", [WS_METHODS.serverPrepareAcpRegistryAgent]: "server", diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index ac60becc2d78a..08048c1e25e73 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -1,6 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { + AuthOrchestrationReadScope, AuthSessionId, EnvironmentId, ProjectId, @@ -9,6 +10,7 @@ import { ProviderInstanceId, WS_METHODS, WsServerRefreshProvidersRpc, + WsRpcGroup, WsSubscribeServerConfigRpc, type OrchestrationV2StoredEvent, type OrchestrationV2ThreadShell, @@ -19,6 +21,7 @@ import { } from "@t3tools/contracts"; import * as Deferred from "effect/Deferred"; import * as DateTime from "effect/DateTime"; +import * as Exit from "effect/Exit"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as PubSub from "effect/PubSub"; @@ -29,6 +32,7 @@ import { Headers } from "effect/http"; import { RpcGroup, RpcMessage, RpcServer } from "effect/rpc"; import * as SqlClient from "effect/sql/SqlClient"; +import * as RpcAuthorization from "./auth/RpcAuthorization.ts"; import * as PreviewAutomationBroker from "./mcp/PreviewAutomationBroker.ts"; import * as ServerBrowser from "./preview/ServerBrowser.ts"; import * as AcpRegistryRuntimeCoordinator from "@t3tools/provider-acp-registry/server/AcpRegistryRuntimeCoordinator"; @@ -869,6 +873,79 @@ it.effect("on-demand clients get provider config without workspace snapshots", ( ), ); +it.effect("server.getSettings returns redacted settings to installed native clients", () => + Effect.gen(function* () { + const settings = yield* ServerSettings.ServerSettingsService; + const saved = yield* settings.updateSettings({ + defaultModelSelection: { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-haiku-4-5", + }, + sourceControlHosts: { gitcafe: { token: "gitcafe-secret" } }, + }); + const exit = yield* Deferred.make>(); + const server = yield* RpcServer.makeNoSerialization(WsRpcGroup, { + ...WS_RPC_SERVER_OPTIONS, + onFromServer: (response) => + response._tag === "Exit" ? Deferred.succeed(exit, response.exit) : Effect.void, + }).pipe( + Effect.provide( + layerWsRpc( + { + sessionId: AuthSessionId.make("settings-test"), + subject: "settings-test", + method: "browser-session-cookie", + scopes: [], + }, + {}, + yield* ServerBrowser.ServerBrowser, + ).pipe( + Layer.provide( + Layer.mergeAll( + layerUnusedConfigRpcServices, + Layer.mock(ExternalLauncher.ExternalLauncher)({}), + Layer.mock(ProviderRegistry.ProviderRegistry)({}), + Layer.mock(UsageLimitSources.UsageLimitSources)({}), + Layer.mock(RemoteOpenTargets.RemoteOpenTargets)({}), + Layer.mock(DirectEndpoints.DirectEndpoints)({}), + Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ + scratchRoot: Effect.succeedNone, + namedProjectsRoot: process.cwd(), + }), + Layer.mock(ServerEnvironment.ServerEnvironment)({}), + Layer.mock(EnvironmentAuth.EnvironmentAuth)({}), + ), + ), + ), + ), + ); + // Swift TestFlight builds send this tag on connect and from the composer. + yield* server.write(0, { + _tag: "Request", + id: RpcMessage.RequestId("settings"), + tag: "server.getSettings", + payload: {}, + headers: Headers.empty, + }); + const result = yield* Deferred.await(exit); + if (!Exit.isSuccess(result)) return yield* Effect.die(result.cause); + const value = result.value as typeof saved; + assert.deepEqual(value, ServerSettings.redactServerSettingsForClient(saved)); + assert.deepEqual(value.defaultModelSelection, saved.defaultModelSelection); + assert.notInclude(JSON.stringify(value), "gitcafe-secret"); + }).pipe( + Effect.provide( + Layer.mergeAll( + Layer.fresh(layerConfigSources), + Layer.mock(PreviewAutomationBroker.PreviewAutomationBroker)({}), + Layer.mock(ServerBrowser.ServerBrowser)({}), + RpcAuthorization.layer([AuthOrchestrationReadScope]), + ), + ), + Effect.scoped, + ), +); + // Compaction keeps the oldest and newest event per message, so a cursor older // than the deleted rows still replays each message where the client would // first have seen it (reducers append on first sight) and reaches the same diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index e1d6303b23506..8a4e937db7351 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -2496,6 +2496,8 @@ export const layerWsRpc = ( Effect.annotateCurrentSpan({ "orchestration_v2.thread_id": input.threadId }).pipe( Effect.andThen(peerLinkRequests.answer(input)), ), + [WS_METHODS.serverGetSettings]: (_input) => + serverSettings.getSettings.pipe(Effect.map(ServerSettings.redactServerSettingsForClient)), [WS_METHODS.serverUpdateSettings]: /* Request instrumentation already owns the span; keep this helper in that context. */ Effect.fnUntraced(function* ({ patch, providerInstanceMutation }) { diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index e4823f30272bd..e5c95874b56ce 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -495,6 +495,7 @@ export const WS_METHODS = { peerLinksLink: "peerLinks.link", peerLinksUnlink: "peerLinks.unlink", peerLinksAnswerRequest: "peerLinks.answerRequest", + serverGetSettings: "server.getSettings", serverUpdateSettings: "server.updateSettings", serverDiscoverSourceControl: "server.discoverSourceControl", serverSearchAcpRegistry: "server.searchAcpRegistry", @@ -793,6 +794,13 @@ const WsPeerLinksAnswerRequestRpc = Rpc.make(WS_METHODS.peerLinksAnswerRequest, error: Schema.Union([PeerLinkRequestError, EnvironmentAuthorizationError]), }); +// No in-tree caller; installed Swift TestFlight builds still request it. +const WsServerGetSettingsRpc = Rpc.make(WS_METHODS.serverGetSettings, { + payload: Schema.Struct({}), + success: ServerSettings, + error: Schema.Union([ServerSettingsError, EnvironmentAuthorizationError]), +}); + const WsServerUpdateSettingsRpc = Rpc.make(WS_METHODS.serverUpdateSettings, { payload: Schema.Struct({ patch: ServerSettingsPatch, @@ -1941,6 +1949,7 @@ export const WsRpcGroup = RpcGroup.make( WsServerRunStorageCleanupRpc, WsServerGetStorageCleanupReportRpc, WsServerGetDatabaseStorageRpc, + WsServerGetSettingsRpc, WsServerUpdateSettingsRpc, WsPeerLinksListRpc, WsPeerLinksLinkRpc, From fd3e0fb566eba3af17d3a8777ee0e19d47170266 Mon Sep 17 00:00:00 2001 From: PR Batch Tester Date: Wed, 28 Oct 2026 12:35:53 +0000 Subject: [PATCH 2/4] test(server): cover model preferences in legacy settings reads --- apps/server/src/ws.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index 08048c1e25e73..95b0aff4ef11e 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -876,11 +876,21 @@ it.effect("on-demand clients get provider config without workspace snapshots", ( it.effect("server.getSettings returns redacted settings to installed native clients", () => Effect.gen(function* () { const settings = yield* ServerSettings.ServerSettingsService; + const provider = ProviderInstanceId.make("claudeAgent"); + const favorites = [{ provider, model: "claude-sonnet-4-5" }]; + const providerModelPreferences = { + [provider]: { + hiddenModels: ["claude-opus-4-5"], + modelOrder: ["claude-sonnet-4-5", "claude-haiku-4-5"], + }, + }; const saved = yield* settings.updateSettings({ defaultModelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), + instanceId: provider, model: "claude-haiku-4-5", }, + favorites, + providerModelPreferences, sourceControlHosts: { gitcafe: { token: "gitcafe-secret" } }, }); const exit = yield* Deferred.make>(); @@ -932,6 +942,8 @@ it.effect("server.getSettings returns redacted settings to installed native clie const value = result.value as typeof saved; assert.deepEqual(value, ServerSettings.redactServerSettingsForClient(saved)); assert.deepEqual(value.defaultModelSelection, saved.defaultModelSelection); + assert.deepEqual(value.favorites, favorites); + assert.deepEqual(value.providerModelPreferences, providerModelPreferences); assert.notInclude(JSON.stringify(value), "gitcafe-secret"); }).pipe( Effect.provide( From d61d08fde1a74bd54c4e394a0b0ab2de14599654 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 23:05:51 +0000 Subject: [PATCH 3/4] fix(server): preserve settings client capabilities after main update --- apps/server/src/ws.test.ts | 8 +++++++- apps/server/src/ws.ts | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index 27240be178562..c0da50abbfcd3 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -1204,6 +1204,8 @@ it.effect("server.getSettings returns redacted settings to installed native clie }, {}, yield* ServerBrowser.ServerBrowser, + () => Effect.void, + { secretReferences: false }, ).pipe( Layer.provide( Layer.mergeAll( @@ -1217,6 +1219,7 @@ it.effect("server.getSettings returns redacted settings to installed native clie scratchRoot: Effect.succeedNone, namedProjectsRoot: process.cwd(), }), + Layer.mock(ScratchThreadPromotion.ScratchThreadPromotion)({}), Layer.mock(ServerEnvironment.ServerEnvironment)({}), Layer.mock(EnvironmentAuth.EnvironmentAuth)({}), ), @@ -1235,7 +1238,10 @@ it.effect("server.getSettings returns redacted settings to installed native clie const result = yield* Deferred.await(exit); if (!Exit.isSuccess(result)) return yield* Effect.die(result.cause); const value = result.value as typeof saved; - assert.deepEqual(value, ServerSettings.redactServerSettingsForClient(saved)); + assert.deepEqual( + value, + ServerSettings.redactServerSettingsForClient(saved, { secretReferences: false }), + ); assert.deepEqual(value.defaultModelSelection, saved.defaultModelSelection); assert.deepEqual(value.favorites, favorites); assert.deepEqual(value.providerModelPreferences, providerModelPreferences); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index ade6aea3569a8..d39d14c242a37 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -2627,7 +2627,11 @@ export const layerWsRpc = ( Effect.andThen(peerLinkRequests.answer(input)), ), [WS_METHODS.serverGetSettings]: (_input) => - serverSettings.getSettings.pipe(Effect.map(ServerSettings.redactServerSettingsForClient)), + serverSettings.getSettings.pipe( + Effect.map((settings) => + ServerSettings.redactServerSettingsForClient(settings, settingsClient), + ), + ), [WS_METHODS.serverUpdateSettings]: /* Request instrumentation already owns the span; keep this helper in that context. */ Effect.fnUntraced(function* ({ patch, providerInstanceMutation }) { From cf84f6082c98076dcf45380e5832a074936ae347 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 4 Nov 2026 20:58:23 +0000 Subject: [PATCH 4/4] fix(server): restore settings regression test after merge --- apps/server/src/ws.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index 81fc9effd3e25..cc01a03eb04c1 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -1259,7 +1259,6 @@ it.effect("server.getSettings returns redacted settings to installed native clie ), ); -it.effect.each(["thread", "shell"] as const)( // Pruned history cannot be replayed: a cursor below the boundary must resync from a snapshot. it.effect.each(["thread", "shell"] as const)( "%s resume from a cursor below the pruned boundary loads a snapshot",