From 42c1d80fc609f47d4535e0cfcbd20d19ba2faac0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 10:29:51 +0000 Subject: [PATCH 1/3] test(server): reproduce #31394 project device access lost in production wiring Builds RuntimeLayer.layerProduction with enableAgentDeviceAccess off globally and on for the thread's project, opens a provider session, and asserts the issued MCP credential carries the "device" capability. On main it does not: ProviderSessionManager is built without ProjectService in production, so any project override resolves as "project not found" and withholds device tools. Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/runtimeLayer.test.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 17b103f75b85f..22b811a495fac 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -55,6 +55,15 @@ import * as OrchestrationEventStore from "../persistence/OrchestrationEventStore import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts"; import * as ProjectService from "../project/ProjectService.ts"; import * as ServerSettings from "../serverSettings.ts"; +import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; +import * as ManagedEndpointRuntime from "../cloud/ManagedEndpointRuntime.ts"; +import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; +import * as TextGeneration from "../textGeneration/TextGeneration.ts"; +import * as ProjectCloneTracker from "../project/ProjectCloneTracker.ts"; +import * as WorktreeSetupTracker from "../project/WorktreeSetupTracker.ts"; +import * as ProviderRegistry from "../provider/ProviderRegistry.ts"; +import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; +import * as McpSessionRegistry from "../mcp/McpSessionRegistry.ts"; import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts"; import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts"; import type { ProviderInstance } from "@t3tools/provider-core/server/driver"; @@ -8683,3 +8692,111 @@ it.layer(layerQueueResumeDisabledInstanceTest)( ); }, ); + +const agentDeviceAccessProjectId = ProjectId.make("project:agent-device-access"); + +const layerAgentDeviceAccessProduction = ( + issued: Deferred.Deferred | undefined>, +) => + RuntimeLayer.layerProduction.pipe( + Layer.provide( + Layer.mergeAll( + Layer.mock(AnalyticsService.AnalyticsService)({}), + Layer.mock(ManagedEndpointRuntime.CloudManagedEndpointRuntime)({}), + Layer.mock(TextGeneration.TextGeneration)({}), + Layer.mock(ProjectCloneTracker.ProjectCloneTracker)({}), + Layer.mock(WorktreeSetupTracker.WorktreeSetupTracker)({}), + Layer.mock(ProviderRegistry.ProviderRegistry)({}), + Layer.mock(GitVcsDriver.GitVcsDriver)({}), + Layer.mock(PreviewManager.PreviewManager)({}), + Layer.mock(TerminalManager.TerminalManager)({}), + Layer.mock(WorkspacePaths.WorkspacePaths)({ + normalizeWorkspaceRoot: (workspaceRoot) => Effect.succeed(workspaceRoot), + }), + Layer.mock(ProjectEnrichmentService.ProjectEnrichmentService)({ + getAvailable: () => + Effect.succeed({ + repositoryIdentity: null, + faviconPath: null, + repositoryIdentityResolved: false, + }), + invalidate: () => Effect.void, + }), + ServerSecretStore.layer, + ManagedPolicy.layerTest(), + ProjectStore.layer, + McpProviderSessions.layer, + Layer.effect( + McpSessionRegistry.McpSessionRegistry, + Effect.map(McpSessionRegistry.McpSessionRegistry, (registry) => ({ + ...registry, + issue: (request) => + Deferred.succeed(issued, request.capabilities).pipe( + Effect.andThen(registry.issue(request)), + ), + })), + ).pipe(Layer.provide(McpSessionRegistryTestkit.layer)), + ), + ), + Layer.provide(SqlitePersistence.layerMemory), + Layer.provide(layerCheckpointStoreTest), + Layer.provide(layerServerConfig), + Layer.provide( + ServerSettings.layerTest({ + enableAgentDeviceAccess: false, + projectSettingsOverrides: { + [agentDeviceAccessProjectId]: { enableAgentDeviceAccess: true }, + }, + }), + ), + Layer.provide(layerTestProviderInstanceRegistry), + Layer.provide(layerGitWorkflowTest), + Layer.provide(layerPlatformTest), + ); + +it.effect("production wiring gives a thread its project's agent device access override", () => + Effect.gen(function* () { + const issued = yield* Deferred.make | undefined>(); + yield* Effect.gen(function* () { + const projects = yield* ProjectService.ProjectService; + const orchestrator = yield* Orchestrator.OrchestratorV2; + const manager = yield* ProviderSessionManager.ProviderSessionManagerV2; + const threadId = ThreadId.make("thread:agent-device-access"); + yield* projects.create({ + commandId: CommandId.make("agent-device-access-project-create"), + projectId: agentDeviceAccessProjectId, + title: "Agent device access", + workspaceRoot: "/tmp/agent-device-access", + }); + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("agent-device-access-thread-create"), + threadId, + projectId: agentDeviceAccessProjectId, + title: "Agent device access", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + // The fixture adapter refuses to open; the credential is issued before that. + yield* manager + .open({ + threadId, + providerSessionId: ProviderSessionId.make("provider-session:agent-device-access"), + modelSelection, + runtimePolicy: { runtimeMode: "full-access", interactionMode: "default", cwd: "/tmp" }, + }) + .pipe(Effect.exit); + }).pipe(Effect.provide(layerAgentDeviceAccessProduction(issued))); + + const capabilities = yield* Deferred.await(issued); + assert.isTrue( + capabilities?.has("device"), + `the project's enableAgentDeviceAccess override must grant device tools; issued: ${[...(capabilities ?? [])].join(", ")}`, + ); + }), +); From 1b78ffb9c3b9ecf4645852c8933e8e677840de87 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 10:38:15 +0000 Subject: [PATCH 2/3] fix(server): agent sessions honour per-project device and browser access ProviderSessionManager read ProjectService optionally. In production its memoized layer is first built through ResourceCleanupService's chain, before layerProduction provides ProjectService, so every project lookup missed. Any project with an enableAgentDeviceAccess or enableAgentBrowserAccess override then withheld that access from every thread. ProjectService is now a required dependency of the session manager, and layerProduction provides it outside every layer that builds the manager. The compiler rejects any ordering where it is missing. Siblings that merged layerProjectService beside layers needing it now take it from that provide. Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/ProviderSessionManager.test.ts | 3 ++- apps/server/src/orchestration-v2/ProviderSessionManager.ts | 7 +++---- apps/server/src/orchestration-v2/runtimeLayer.ts | 7 ++----- .../src/orchestration-v2/testkit/ProviderReplayHarness.ts | 2 ++ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts index b1e05577bfbae..2dd18c646d118 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts @@ -669,7 +669,8 @@ function layerTest(input: { layerConfiguredMcpRegistry, layerTestStores, ...(input.serverSettingsLayer === undefined ? [] : [input.serverSettingsLayer]), - ...(input.projectServiceLayer === undefined ? [] : [input.projectServiceLayer]), + input.projectServiceLayer ?? + Layer.mock(ProjectService.ProjectService)({ getById: () => Effect.succeedNone }), ), ), ), diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.ts index f06011676c667..0199590df46a0 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.ts @@ -326,6 +326,7 @@ export const layerWithOptions = ( | IdAllocator.IdAllocatorV2 | McpProviderSessions.McpProviderSessions | McpSessionRegistry.McpSessionRegistry + | ProjectService.ProjectService | ProjectionStore.ProjectionStoreV2 | ProviderEventIngestor.ProviderEventIngestorV2 | ProviderAdapterRegistry.ProviderAdapterRegistryV2 @@ -339,7 +340,7 @@ export const layerWithOptions = ( const mcpSessions = yield* McpProviderSessions.McpProviderSessions; /** Unreadable settings must preserve explicit browser-access opt-outs. */ const serverSettings = yield* Effect.serviceOption(ServerSettings.ServerSettingsService); - const projectService = yield* Effect.serviceOption(ProjectService.ProjectService); + const projectService = yield* ProjectService.ProjectService; const eventSink = yield* EventSink.EventSinkV2; const commandOutput = yield* CommandOutputHub.CommandOutputHub; const idAllocator = yield* IdAllocator.IdAllocatorV2; @@ -360,9 +361,7 @@ export const layerWithOptions = ( (entry) => entry.enableAgentDeviceAccess !== undefined, ); if (browserOverridden || deviceOverridden) { - const project = Option.isSome(projectService) - ? yield* projectService.value.getById(thread.projectId) - : Option.none(); + const project = yield* projectService.getById(thread.projectId); if (Option.isNone(project)) return { browser: browserOverridden ? false : settings.enableAgentBrowserAccess, diff --git a/apps/server/src/orchestration-v2/runtimeLayer.ts b/apps/server/src/orchestration-v2/runtimeLayer.ts index 1b54627595372..fedce34d69cc6 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.ts @@ -238,7 +238,6 @@ const layerAgentSessionImporterProvided = AgentSessionImporter.layer.pipe( Layer.provide( Layer.mergeAll( AgentSessionScanner.layer, - layerProjectService, layerOrchestratorProvided, layerEventSink, IdAllocator.layer, @@ -268,7 +267,6 @@ const layerManagedProjectFoldersProvided = ManagedProjectFolders.layer.pipe( const layerThreadLaunchProvided = ThreadLaunchService.layer.pipe( Layer.provide( Layer.mergeAll( - layerProjectService, layerProjectSetupScriptRunner, layerManagedProjectFoldersProvided, layerThreadManagementProvided, @@ -293,7 +291,6 @@ const layerScheduledTaskProvided = ScheduledTaskService.layer.pipe( const layerScratchThreadPromotionProvided = ScratchThreadPromotion.layer.pipe( Layer.provide( Layer.mergeAll( - layerProjectService, layerManagedProjectFoldersProvided, layerThreadManagementProvided, layerScheduledTaskProvided, @@ -368,8 +365,7 @@ export const layer = Layer.mergeAll( ); export const layerProduction = Layer.mergeAll( - layer.pipe(Layer.provide(layerProjectService)), - layerProjectService, + layer, layerManagedProjectFoldersProvided, layerThreadLaunchProvided, layerScheduledTaskProvided, @@ -393,5 +389,6 @@ export const layerProduction = Layer.mergeAll( ).pipe( Layer.provide(ResourceCleanupService.layer.pipe(Layer.provide(layerScheduledTaskProvided))), Layer.provide(Scheduler.layer), + Layer.provideMerge(layerProjectService), Layer.provideMerge(layerEventInfrastructure), ); diff --git a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts index e4cac1bd4adfc..b4e2766851879 100644 --- a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts +++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts @@ -41,6 +41,7 @@ import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as Orchestrator from "../Orchestrator.ts"; import * as ProjectionStore from "../ProjectionStore.ts"; import * as ProjectStore from "../ProjectStore.ts"; +import * as ProjectService from "../../project/ProjectService.ts"; import * as ProviderAdapterRegistry from "../ProviderAdapterRegistry.ts"; import * as ProviderAuthService from "../../provider/ProviderAuthService.ts"; import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; @@ -392,6 +393,7 @@ export function layerWithRegistry( McpSessionRegistryTestkit.layer, layerProviderEventIngestorProvided, layerStores, + Layer.mock(ProjectService.ProjectService)({ getById: () => Effect.succeedNone }), ), ), ); From d6dfd757fe02e04d9f940539661155c3d936b0e6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 22:04:43 +0000 Subject: [PATCH 3/3] test(server): mock services main's production layer now needs AI-assisted; verified with the tests and tools listed above. --- apps/server/src/orchestration-v2/runtimeLayer.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 0fee14423f9a6..ae0ffaba27c27 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -65,6 +65,8 @@ import * as WorktreeSetupTracker from "../project/WorktreeSetupTracker.ts"; import * as ProviderRegistry from "../provider/ProviderRegistry.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as McpSessionRegistry from "../mcp/McpSessionRegistry.ts"; +import * as SecretResolver from "@t3tools/secret-provider-core/server/SecretResolver"; +import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts"; import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts"; import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts"; import type { ProviderInstance } from "@t3tools/provider-core/server/driver"; @@ -9190,6 +9192,8 @@ const layerAgentDeviceAccessProduction = ( Layer.mock(WorktreeSetupTracker.WorktreeSetupTracker)({}), Layer.mock(ProviderRegistry.ProviderRegistry)({}), Layer.mock(GitVcsDriver.GitVcsDriver)({}), + Layer.mock(SecretResolver.SecretResolver)({}), + Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({}), Layer.mock(PreviewManager.PreviewManager)({}), Layer.mock(TerminalManager.TerminalManager)({}), Layer.mock(WorkspacePaths.WorkspacePaths)({