1import {
2 CLIENT_GUARDED_RPC_SCOPES,
3 type DeviceListInput,
4 clientRpcRequiredScopes,
5 authScopeRequiredResponse,
6 AssetCreateUrlInput,
7 AuthAccessReadScope,
8 ServerSettingsPatch,
9 ProviderInstanceMutation,
10 requiredScopesForServerSettingsPatch,
11 requiredScopesForProjectMutation,
12 ProjectMutation,
13 AuthSettingsWriteScope,
14 AuthProvidersManageScope,
15 AuthEnvironmentMaintainScope,
16 AuthFilesystemReadScope,
17 AuthFilesystemWriteScope,
18 AuthDiagnosticsReadScope,
19 AuthOrchestrationOperateScope,
20 AuthOrchestrationReadScope,
21 AuthPreviewOperateScope,
22 AuthRelayReadScope,
23 AuthRelayWriteScope,
24 AuthTerminalOperateScope,
25 ORCHESTRATION_V2_WS_METHODS,
26 AuthTerminalReadScope,
27 type AuthEnvironmentScope,
28 EnvironmentAuthorizationError,
29 RpcScopeAuthorization,
30 WS_METHODS,
31 WsRpcGroup,
32} from "@t3tools/contracts";
33import * as Effect from "effect/Effect";
34import * as Schema from "effect/Schema";
35import * as Layer from "effect/Layer";
36import type * as RpcGroup from "effect/rpc/RpcGroup";
37
38type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"];
39
40/**
41 * Keep authorization coverage coupled to the RPC group itself. Adding an RPC to
42 * `WsRpcGroup` without choosing a scope is a type error instead of a production
43 * runtime failure.
44 */
45export const RPC_REQUIRED_SCOPES = {
46 ...CLIENT_GUARDED_RPC_SCOPES,
47 [WS_METHODS.uiSettingsGet]: AuthOrchestrationReadScope,
48 [WS_METHODS.uiSettingsSubscribe]: AuthOrchestrationReadScope,
49 [WS_METHODS.managedWorkspacesStatus]: AuthFilesystemReadScope,
50 [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: AuthOrchestrationOperateScope,
51 [ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: AuthOrchestrationReadScope,
52 [ORCHESTRATION_V2_WS_METHODS.stopWorkflow]: AuthOrchestrationOperateScope,
53 [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope,
54 [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: AuthOrchestrationReadScope,
55 [ORCHESTRATION_V2_WS_METHODS.searchThreads]: AuthOrchestrationReadScope,
56 [ORCHESTRATION_V2_WS_METHODS.searchThread]: AuthOrchestrationReadScope,
57 [ORCHESTRATION_V2_WS_METHODS.searchThreadStream]: AuthOrchestrationReadScope,
58 [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: AuthOrchestrationReadScope,
59 [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: AuthOrchestrationReadScope,
60 [ORCHESTRATION_V2_WS_METHODS.getTurnItem]: AuthOrchestrationReadScope,
61 [ORCHESTRATION_V2_WS_METHODS.launchThread]: AuthOrchestrationOperateScope,
62 [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: AuthOrchestrationReadScope,
63 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: AuthOrchestrationReadScope,
64 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: AuthOrchestrationReadScope,
65 [WS_METHODS.projectsMutate]: AuthOrchestrationOperateScope,
66 [WS_METHODS.serverProbe]: AuthOrchestrationReadScope,
67 [WS_METHODS.serverGetConfig]: AuthOrchestrationReadScope,
68 [WS_METHODS.serverRefreshProviders]: AuthOrchestrationReadScope,
69 [WS_METHODS.serverUpdateProvider]: AuthProvidersManageScope,
70 [WS_METHODS.providerAuthStart]: AuthProvidersManageScope,
71 [WS_METHODS.providerConsumeResetCredit]: AuthProvidersManageScope,
72 [WS_METHODS.providerAuthComplete]: AuthProvidersManageScope,
73 [WS_METHODS.chatGptReconnectProfile]: AuthProvidersManageScope,
74 [WS_METHODS.chatGptImportProfile]: AuthProvidersManageScope,
75 [WS_METHODS.chatGptHandoffSubscribe]: AuthProvidersManageScope,
76 [WS_METHODS.codexAuthCallbackSubscribe]: AuthProvidersManageScope,
77 [WS_METHODS.providerAuthRespond]: AuthProvidersManageScope,
78 [WS_METHODS.providerAuthCancel]: AuthProvidersManageScope,
79 [WS_METHODS.providerAuthLogout]: AuthProvidersManageScope,
80 [WS_METHODS.providerAuthSubscribe]: AuthProvidersManageScope,
81 [WS_METHODS.providerInstallStart]: AuthProvidersManageScope,
82 [WS_METHODS.providerInstallCancel]: AuthProvidersManageScope,
83 [WS_METHODS.providerInstallSubscribe]: AuthOrchestrationReadScope,
84 [WS_METHODS.providerInstallRemove]: AuthProvidersManageScope,
85 [WS_METHODS.serverUpdateServer]: AuthEnvironmentMaintainScope,
86 [WS_METHODS.serverUpdateServerWithProgress]: AuthEnvironmentMaintainScope,
87 [WS_METHODS.serverCommitDesktopUpdate]: AuthEnvironmentMaintainScope,
88 [WS_METHODS.serverUpsertKeybinding]: AuthSettingsWriteScope,
89 [WS_METHODS.serverRemoveKeybinding]: AuthSettingsWriteScope,
90 [WS_METHODS.serverGetStorageCleanupReport]: AuthOrchestrationReadScope,
91 [WS_METHODS.serverGetSettings]: AuthOrchestrationReadScope,
92 [WS_METHODS.serverUpdateSettings]: AuthSettingsWriteScope,
93 [WS_METHODS.serverSearchAcpRegistry]: AuthOrchestrationReadScope,
94 [WS_METHODS.serverPrepareAcpRegistryAgent]: AuthProvidersManageScope,
95 [WS_METHODS.serverUninstallAcpRegistryManagedBinary]: AuthProvidersManageScope,
96 [WS_METHODS.serverAcceptAcpRegistryUrlAuth]: AuthProvidersManageScope,
97 [WS_METHODS.serverListAcpRegistrySessions]: AuthOrchestrationReadScope,
98 [WS_METHODS.serverImportAcpRegistrySession]: AuthOrchestrationOperateScope,
99 [WS_METHODS.serverDeleteAcpRegistrySession]: AuthOrchestrationOperateScope,
100 [WS_METHODS.serverListAcpRegistryProviders]: AuthOrchestrationReadScope,
101 [WS_METHODS.serverSetAcpRegistryProvider]: AuthProvidersManageScope,
102 [WS_METHODS.serverDisableAcpRegistryProvider]: AuthProvidersManageScope,
103 [WS_METHODS.serverLogoutAcpRegistry]: AuthProvidersManageScope,
104 [WS_METHODS.serverDiscoverSourceControl]: AuthOrchestrationReadScope,
105 [WS_METHODS.serverGetTraceDiagnostics]: AuthDiagnosticsReadScope,
106 [WS_METHODS.serverGetProcessDiagnostics]: AuthDiagnosticsReadScope,
107 // Load-balancing new threads reads host load; that is part of operating
108 // threads, not of inspecting diagnostics.
109 [WS_METHODS.serverGetHostResources]: AuthOrchestrationReadScope,
110 [WS_METHODS.serverGetProcessResourceHistory]: AuthDiagnosticsReadScope,
111 [WS_METHODS.serverGetResourceTelemetryHistory]: AuthDiagnosticsReadScope,
112 [WS_METHODS.serverRetryResourceTelemetry]: AuthDiagnosticsReadScope,
113 [WS_METHODS.serverGetUsageSummary]: AuthDiagnosticsReadScope,
114 [WS_METHODS.serverRefreshUsageRates]: AuthDiagnosticsReadScope,
115 [WS_METHODS.serverSignalProcess]: AuthEnvironmentMaintainScope,
116 [WS_METHODS.serverReportClientActivity]: AuthOrchestrationReadScope,
117 [WS_METHODS.serverReportHostPowerState]: AuthEnvironmentMaintainScope,
118 [WS_METHODS.serverGetBackgroundPolicy]: AuthOrchestrationReadScope,
119 [WS_METHODS.scheduledTasksList]: AuthOrchestrationReadScope,
120 [WS_METHODS.scheduledTasksSubscribe]: AuthOrchestrationReadScope,
121 [WS_METHODS.secretsAnswerRequest]: AuthOrchestrationOperateScope,
122 // Delivery logs hold request bodies, so they need the same scope as the URL.
123 [WS_METHODS.scheduledTasksListWebhookDeliveries]: AuthOrchestrationOperateScope,
124 [WS_METHODS.scheduledTasksGetWebhookDelivery]: AuthOrchestrationOperateScope,
125 [WS_METHODS.cloudGetRelayClientStatus]: AuthRelayReadScope,
126 [WS_METHODS.cloudInstallRelayClient]: AuthRelayWriteScope,
127 [WS_METHODS.mergebotQueue]: AuthOrchestrationReadScope,
128 [WS_METHODS.mergebotSubscribe]: AuthOrchestrationReadScope,
129 [WS_METHODS.mergebotSubscribePullRequestStatus]: AuthOrchestrationReadScope,
130 [WS_METHODS.mergebotSubscribeDeployments]: AuthOrchestrationReadScope,
131 [WS_METHODS.jjSubscribeStack]: AuthFilesystemReadScope,
132 [WS_METHODS.jjSnapshot]: AuthFilesystemReadScope,
133 [WS_METHODS.jjDiff]: AuthFilesystemReadScope,
134 [WS_METHODS.pullRequestsList]: AuthOrchestrationReadScope,
135 [WS_METHODS.pullRequestsListStats]: AuthOrchestrationReadScope,
136 [WS_METHODS.pullRequestsSummary]: AuthOrchestrationReadScope,
137 [WS_METHODS.pullRequestsRouting]: AuthOrchestrationReadScope,
138 [WS_METHODS.pullRequestsRoutingIdentity]: AuthOrchestrationReadScope,
139 [WS_METHODS.pullRequestsStack]: AuthOrchestrationReadScope,
140 [WS_METHODS.pullRequestsLinkedThreads]: AuthOrchestrationReadScope,
141 [WS_METHODS.pullRequestsDetail]: AuthOrchestrationReadScope,
142 [WS_METHODS.pullRequestsPreview]: AuthOrchestrationReadScope,
143 [WS_METHODS.pullRequestsChecks]: AuthOrchestrationReadScope,
144 [WS_METHODS.pullRequestsActivity]: AuthOrchestrationReadScope,
145 [WS_METHODS.pullRequestsReviewGuide]: AuthOrchestrationReadScope,
146 [WS_METHODS.pullRequestsThreadComments]: AuthOrchestrationReadScope,
147 [WS_METHODS.pullRequestsDiffFileContents]: AuthOrchestrationReadScope,
148 [WS_METHODS.pullRequestsFilesViewed]: AuthOrchestrationReadScope,
149 // Read scope like the reads it un-caches: refreshing is part of reading, and a read-only
150 // client pressing refresh must not be told it may not look again.
151 [WS_METHODS.pullRequestsInvalidate]: AuthOrchestrationReadScope,
152 [WS_METHODS.pullRequestsReportState]: AuthOrchestrationReadScope,
153 [WS_METHODS.pullRequestsSubscribeRefreshes]: AuthOrchestrationReadScope,
154 // The candidate list is a read like the detail beside it; asking somebody for a review is a
155 // write like every other one.
156 [WS_METHODS.pullRequestsReviewerCandidates]: AuthOrchestrationReadScope,
157 [WS_METHODS.pullRequestsLabelCandidates]: AuthOrchestrationReadScope,
158 [WS_METHODS.sourceControlLookupRepository]: AuthOrchestrationReadScope,
159 [WS_METHODS.subscribeProjectClones]: AuthOrchestrationReadScope,
160 [WS_METHODS.projectsListEntries]: AuthFilesystemReadScope,
161 [WS_METHODS.projectsReadFile]: AuthFilesystemReadScope,
162 [WS_METHODS.projectsWatch]: AuthFilesystemReadScope,
163 [WS_METHODS.projectsSearchContents]: AuthFilesystemReadScope,
164 [WS_METHODS.projectsSearchEntries]: AuthFilesystemReadScope,
165 [WS_METHODS.projectsWriteFile]: AuthFilesystemWriteScope,
166 [WS_METHODS.projectsEnsureScratch]: AuthOrchestrationOperateScope,
167 [WS_METHODS.projectsCreateNew]: AuthOrchestrationOperateScope,
168 [WS_METHODS.shellOpenInEditor]: AuthOrchestrationOperateScope,
169 [WS_METHODS.filesystemBrowse]: AuthFilesystemReadScope,
170 [WS_METHODS.filesystemGetMetadata]: AuthFilesystemReadScope,
171 [WS_METHODS.agentSessionsScan]: AuthOrchestrationReadScope,
172 [WS_METHODS.agentSessionsImport]: AuthOrchestrationOperateScope,
173 [WS_METHODS.assetsCreateUrl]: AuthOrchestrationReadScope,
174 [WS_METHODS.assetsPersistChatAttachments]: AuthOrchestrationOperateScope,
175 [WS_METHODS.attachmentsCreateUploadUrl]: AuthOrchestrationOperateScope,
176 [WS_METHODS.attachmentsDelete]: AuthOrchestrationOperateScope,
177 [WS_METHODS.providerUploadFeedback]: AuthOrchestrationOperateScope,
178 // An app's tool calls can change things on its server, like a user action.
179 [WS_METHODS.mcpAppsCallTool]: AuthOrchestrationOperateScope,
180 [WS_METHODS.mcpAppsToolInfo]: AuthOrchestrationReadScope,
181 [WS_METHODS.mcpAppsReadResource]: AuthOrchestrationReadScope,
182 [WS_METHODS.mcpAppsUpdateModelContext]: AuthOrchestrationOperateScope,
183 [WS_METHODS.subscribeVcsStatus]: AuthOrchestrationReadScope,
184 [WS_METHODS.subscribeWorktreeSetup]: AuthOrchestrationReadScope,
185 [WS_METHODS.worktreeSetupCancel]: AuthOrchestrationOperateScope,
186 [WS_METHODS.subscribeResourceTelemetry]: AuthDiagnosticsReadScope,
187 [WS_METHODS.vcsRefreshStatus]: AuthOrchestrationReadScope,
188 [WS_METHODS.gitResolvePullRequest]: AuthOrchestrationReadScope,
189 [WS_METHODS.vcsListRefs]: AuthOrchestrationReadScope,
190 [WS_METHODS.reviewGetDiffPreview]: AuthFilesystemReadScope,
191 [WS_METHODS.reviewGetDiffFileContents]: AuthFilesystemReadScope,
192 [WS_METHODS.terminalOpen]: AuthTerminalOperateScope,
193 [WS_METHODS.terminalAttach]: AuthTerminalOperateScope,
194 [WS_METHODS.terminalObserve]: AuthTerminalReadScope,
195 [WS_METHODS.terminalWrite]: AuthTerminalOperateScope,
196 [WS_METHODS.terminalResize]: AuthTerminalOperateScope,
197 [WS_METHODS.terminalClear]: AuthTerminalOperateScope,
198 [WS_METHODS.terminalRestart]: AuthTerminalOperateScope,
199 [WS_METHODS.terminalClose]: AuthTerminalOperateScope,
200 [WS_METHODS.subscribeTerminalEvents]: AuthTerminalReadScope,
201 [WS_METHODS.subscribeTerminalMetadata]: AuthTerminalReadScope,
202 [WS_METHODS.subscribeScriptProgress]: AuthOrchestrationReadScope,
203 [WS_METHODS.previewOpen]: AuthPreviewOperateScope,
204 [WS_METHODS.previewNavigate]: AuthPreviewOperateScope,
205 [WS_METHODS.previewResize]: AuthPreviewOperateScope,
206 [WS_METHODS.previewAdjust]: AuthPreviewOperateScope,
207 [WS_METHODS.previewRefresh]: AuthPreviewOperateScope,
208 [WS_METHODS.previewClose]: AuthPreviewOperateScope,
209 [WS_METHODS.previewList]: AuthOrchestrationReadScope,
210 [WS_METHODS.previewClearProfile]: AuthPreviewOperateScope,
211 [WS_METHODS.previewReportProfiles]: AuthPreviewOperateScope,
212 [WS_METHODS.previewReportStatus]: AuthPreviewOperateScope,
213 [WS_METHODS.subscribePreviewEvents]: AuthOrchestrationReadScope,
214 [WS_METHODS.subscribeDiscoveredLocalServers]: AuthOrchestrationReadScope,
215 [WS_METHODS.deviceConfigure]: AuthSettingsWriteScope,
216 [WS_METHODS.deviceTestHost]: AuthSettingsWriteScope,
217 [WS_METHODS.deviceList]: AuthOrchestrationReadScope,
218 [WS_METHODS.deviceOpen]: AuthOrchestrationOperateScope,
219 [WS_METHODS.deviceClose]: AuthOrchestrationOperateScope,
220 [WS_METHODS.deviceShutdown]: AuthOrchestrationOperateScope,
221 [WS_METHODS.deviceDetail]: AuthOrchestrationReadScope,
222 [WS_METHODS.deviceAction]: AuthOrchestrationOperateScope,
223 [WS_METHODS.subscribeDeviceState]: AuthOrchestrationReadScope,
224 [WS_METHODS.subscribeServerConfig]: AuthOrchestrationReadScope,
225 [WS_METHODS.subscribeServerLifecycle]: AuthOrchestrationReadScope,
226 [WS_METHODS.subscribeAuthAccess]: AuthAccessReadScope,
227 [WS_METHODS.subscribeBackgroundPolicy]: AuthOrchestrationReadScope,
228} as const satisfies Readonly<Record<WsRpcMethod, AuthEnvironmentScope>>;
229
230export function requiredScopeForRpcMethod(method: string): AuthEnvironmentScope {
231 if (!Object.hasOwn(RPC_REQUIRED_SCOPES, method)) {
232 throw new Error(`RPC method ${method} has no declared authorization scope.`);
233 }
234 const requiredScope = RPC_REQUIRED_SCOPES[method as WsRpcMethod];
235 if (requiredScope === undefined) {
236 throw new Error(`RPC method ${method} has no declared authorization scope.`);
237 }
238 return requiredScope;
239}
240
241export const rpcAuthorizationError = (requiredScope: AuthEnvironmentScope) =>
242 new EnvironmentAuthorizationError({
243 message: `The authenticated token is missing required scope: ${requiredScope}.`,
244 ...authScopeRequiredResponse(requiredScope),
245 });
246
247const SettingsUpdate = Schema.Struct({
248 patch: ServerSettingsPatch,
249 providerInstanceMutation: Schema.optionalKey(ProviderInstanceMutation),
250});
251
252const requiredScopesForSettingsUpdate = (payload: unknown) => {
253 const input = Schema.decodeUnknownSync(SettingsUpdate)(payload);
254 const scopes = requiredScopesForServerSettingsPatch(input.patch);
255 if (input.providerInstanceMutation === undefined) return scopes;
256 // An atomic provider mutation carries an empty patch unless it also changes settings.
257 return Object.values(input.patch).every((value) => value === undefined)
258 ? [AuthProvidersManageScope]
259 : [...new Set([...scopes, AuthProvidersManageScope])];
260};
261
262const decodeProjectMutation = Schema.decodeUnknownSync(ProjectMutation);
263
264const requiredScopesForRpcCall = (
265 method: string,
266 payload: unknown,
267): ReadonlyArray<AuthEnvironmentScope> => {
268 if (method === WS_METHODS.serverRetryResourceTelemetry) {
269 return [AuthEnvironmentMaintainScope, AuthDiagnosticsReadScope];
270 }
271 if (method === WS_METHODS.assetsCreateUrl) {
272 const { resource } = Schema.decodeUnknownSync(AssetCreateUrlInput)(payload);
273 return [
274 resource._tag === "workspace-file" ||
275 resource._tag === "media-file" ||
276 resource._tag === "draft-workspace-file"
277 ? AuthFilesystemReadScope
278 : AuthOrchestrationReadScope,
279 ];
280 }
281 if (method === WS_METHODS.serverUpdateSettings) return requiredScopesForSettingsUpdate(payload);
282 if (method === WS_METHODS.projectsMutate) {
283 return requiredScopesForProjectMutation(decodeProjectMutation(payload));
284 }
285 const guarded = clientRpcRequiredScopes(method, payload);
286 if (guarded.length > 0) return guarded;
287 return [requiredScopeForRpcMethod(method)];
288};
289
290/** Authorizes every RPC on one connection against that connection's session scopes. */
291export const layer = (scopes: ReadonlyArray<AuthEnvironmentScope>) =>
292 Layer.succeed(RpcScopeAuthorization)((effect, { rpc, payload }) => {
293 const requiredScopes = requiredScopesForRpcCall(rpc._tag, payload);
294 const requiredScope = requiredScopes.find((scope) => !scopes.includes(scope));
295 return requiredScope === undefined ? effect : Effect.fail(rpcAuthorizationError(requiredScope));
296 });
297
298/** Retrying can install or restart tools even though ordinary listing is readable. */
299export const requiredScopeForDeviceList = (input: DeviceListInput): AuthEnvironmentScope =>
300 input.retryHostId || input.updateTool
301 ? AuthOrchestrationOperateScope
302 : AuthOrchestrationReadScope;