diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts index b4fc69727c1de..1a78e4899b3d4 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts @@ -688,7 +688,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 0fb31e81a0732..5d0654cab0eeb 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.ts @@ -337,6 +337,7 @@ export const layerWithOptions = ( | IdAllocator.IdAllocatorV2 | McpProviderSessions.McpProviderSessions | McpSessionRegistry.McpSessionRegistry + | ProjectService.ProjectService | ProjectionStore.ProjectionStoreV2 | ProviderEventIngestor.ProviderEventIngestorV2 | ProviderAdapterRegistry.ProviderAdapterRegistryV2 @@ -350,7 +351,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 promptSuggestionStore = yield* SubscriptionRef.make< @@ -382,9 +383,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.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index a49e323770cc1..ae0ffaba27c27 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -56,6 +56,17 @@ 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 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"; @@ -9165,3 +9176,113 @@ 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(SecretResolver.SecretResolver)({}), + Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({}), + 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(", ")}`, + ); + }), +); diff --git a/apps/server/src/orchestration-v2/runtimeLayer.ts b/apps/server/src/orchestration-v2/runtimeLayer.ts index ba1dadad1864c..62d6792d3ffc1 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.ts @@ -239,7 +239,6 @@ const layerAgentSessionImporterProvided = AgentSessionImporter.layer.pipe( Layer.provide( Layer.mergeAll( AgentSessionScanner.layer, - layerProjectService, layerOrchestratorProvided, layerEventSink, IdAllocator.layer, @@ -269,7 +268,6 @@ const layerManagedProjectFoldersProvided = ManagedProjectFolders.layer.pipe( const layerThreadLaunchProvided = ThreadLaunchService.layer.pipe( Layer.provide( Layer.mergeAll( - layerProjectService, layerProjectSetupScriptRunner, layerManagedProjectFoldersProvided, layerThreadManagementProvided, @@ -294,7 +292,6 @@ const layerScheduledTaskProvided = ScheduledTaskService.layer.pipe( const layerScratchThreadPromotionProvided = ScratchThreadPromotion.layer.pipe( Layer.provide( Layer.mergeAll( - layerProjectService, layerManagedProjectFoldersProvided, layerThreadManagementProvided, layerScheduledTaskProvided, @@ -372,8 +369,7 @@ export const layerProduction = Layer.mergeAll( WorktreeBranchSync.layer.pipe( Layer.provide(Layer.merge(layerThreadManagementProvided, ProjectionStore.layer)), ), - layer.pipe(Layer.provide(layerProjectService)), - layerProjectService, + layer, layerManagedProjectFoldersProvided, layerThreadLaunchProvided, layerScheduledTaskProvided, @@ -397,5 +393,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 ae0d5ef346d34..926a6a865054b 100644 --- a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts +++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts @@ -42,6 +42,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"; @@ -394,6 +395,7 @@ export function layerWithRegistry( McpSessionRegistryTestkit.layer, layerProviderEventIngestorProvided, layerStores, + Layer.mock(ProjectService.ProjectService)({ getById: () => Effect.succeedNone }), ), ), );