Skip to content
Source
Sign inSign up

RpcAuthorization.ts

306 lines16.5 KB

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​
40export const RPC_REQUIRED_SCOPES = {
41 ...CLIENT_GUARDED_RPC_SCOPES,
42 [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: AuthOrchestrationOperateScope,
43 [ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: AuthOrchestrationReadScope,
44 [ORCHESTRATION_V2_WS_METHODS.getNativeChildWork]: AuthOrchestrationReadScope,
45 [ORCHESTRATION_V2_WS_METHODS.stopWorkflow]: AuthOrchestrationOperateScope,
46 [ORCHESTRATION_V2_WS_METHODS.forceStopThread]: AuthOrchestrationOperateScope,
47 [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope,
48 [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: AuthOrchestrationReadScope,
49 [ORCHESTRATION_V2_WS_METHODS.searchThreads]: AuthOrchestrationReadScope,
50 [ORCHESTRATION_V2_WS_METHODS.searchThread]: AuthOrchestrationReadScope,
51 [ORCHESTRATION_V2_WS_METHODS.searchThreadStream]: AuthOrchestrationReadScope,
52 [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: AuthOrchestrationReadScope,
53 [ORCHESTRATION_V2_WS_METHODS.getThreadFamily]: AuthOrchestrationReadScope,
54 [ORCHESTRATION_V2_WS_METHODS.getThreadTitles]: AuthOrchestrationReadScope,
55 [ORCHESTRATION_V2_WS_METHODS.getThreadLinkMetadata]: AuthOrchestrationReadScope,
56 [ORCHESTRATION_V2_WS_METHODS.getWorktreeThreadIds]: AuthOrchestrationReadScope,
57 [ORCHESTRATION_V2_WS_METHODS.getOlderSettledThreads]: AuthOrchestrationReadScope,
58 [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: AuthOrchestrationReadScope,
59 [ORCHESTRATION_V2_WS_METHODS.getTurnItem]: AuthOrchestrationReadScope,
60 [ORCHESTRATION_V2_WS_METHODS.launchThread]: AuthOrchestrationOperateScope,
61 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: AuthOrchestrationReadScope,
62 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: AuthOrchestrationReadScope,
63 [ORCHESTRATION_V2_WS_METHODS.subscribeCommandOutput]: AuthOrchestrationReadScope,
64 [ORCHESTRATION_V2_WS_METHODS.subscribePromptSuggestion]: AuthOrchestrationReadScope,
65 [WS_METHODS.projectsMutate]: AuthOrchestrationOperateScope,
66 [WS_METHODS.serverProbe]: AuthOrchestrationReadScope,
67 [WS_METHODS.serverGetConfig]: AuthOrchestrationReadScope,
68 [WS_METHODS.serverGetNpmRelease]: AuthOrchestrationReadScope,
69 [WS_METHODS.serverRefreshProviders]: AuthOrchestrationReadScope,
70 [WS_METHODS.serverUpdateProvider]: AuthProvidersManageScope,
71 [WS_METHODS.providerAuthStart]: AuthProvidersManageScope,
72 [WS_METHODS.providerConsumeResetCredit]: AuthProvidersManageScope,
73 [WS_METHODS.providerAuthComplete]: AuthProvidersManageScope,
74 [WS_METHODS.chatGptReconnectProfile]: AuthProvidersManageScope,
75 [WS_METHODS.chatGptImportProfile]: AuthProvidersManageScope,
76 [WS_METHODS.chatGptHandoffSubscribe]: AuthProvidersManageScope,
77 [WS_METHODS.providerAuthRespond]: AuthProvidersManageScope,
78 [WS_METHODS.providerAuthCancel]: AuthProvidersManageScope,
79 [WS_METHODS.providerAuthRemoveMethod]: AuthProvidersManageScope,
80 [WS_METHODS.providerAuthLogout]: AuthProvidersManageScope,
81 [WS_METHODS.providerAuthSubscribe]: AuthProvidersManageScope,
82 [WS_METHODS.providerInstallStart]: AuthProvidersManageScope,
83 [WS_METHODS.providerInstallCancel]: AuthProvidersManageScope,
84 [WS_METHODS.providerInstallSubscribe]: AuthOrchestrationReadScope,
85 [WS_METHODS.providerInstallRemove]: AuthProvidersManageScope,
86 [WS_METHODS.serverUpdateServer]: AuthEnvironmentMaintainScope,
87 [WS_METHODS.serverUpdateServerWithProgress]: AuthEnvironmentMaintainScope,
88 [WS_METHODS.serverCommitDesktopUpdate]: AuthEnvironmentMaintainScope,
89 [WS_METHODS.serverUpsertKeybinding]: AuthSettingsWriteScope,
90 [WS_METHODS.serverRemoveKeybinding]: AuthSettingsWriteScope,
91 [WS_METHODS.serverGetStorageCleanupReport]: AuthOrchestrationReadScope,
92 [WS_METHODS.serverGetDatabaseStorage]: AuthOrchestrationReadScope,
93 [WS_METHODS.peerLinksList]: AuthAccessReadScope,
94 [WS_METHODS.serverUpdateSettings]: AuthSettingsWriteScope,
95 [WS_METHODS.serverSearchAcpRegistry]: AuthOrchestrationReadScope,
96 [WS_METHODS.serverPrepareAcpRegistryAgent]: AuthProvidersManageScope,
97 [WS_METHODS.serverUninstallAcpRegistryManagedBinary]: AuthProvidersManageScope,
98 [WS_METHODS.serverAcceptAcpRegistryUrlAuth]: AuthProvidersManageScope,
99 [WS_METHODS.serverListAcpRegistrySessions]: AuthOrchestrationReadScope,
100 [WS_METHODS.serverImportAcpRegistrySession]: AuthOrchestrationOperateScope,
101 [WS_METHODS.serverDeleteAcpRegistrySession]: AuthOrchestrationOperateScope,
102 [WS_METHODS.serverListAcpRegistryProviders]: AuthOrchestrationReadScope,
103 [WS_METHODS.serverSetAcpRegistryProvider]: AuthProvidersManageScope,
104 [WS_METHODS.serverDisableAcpRegistryProvider]: AuthProvidersManageScope,
105 [WS_METHODS.serverLogoutAcpRegistry]: AuthProvidersManageScope,
106 [WS_METHODS.serverDiscoverSourceControl]: AuthOrchestrationReadScope,
107 [WS_METHODS.serverGetTraceDiagnostics]: AuthDiagnosticsReadScope,
108 [WS_METHODS.serverGetProcessDiagnostics]: AuthDiagnosticsReadScope,
109 // Thread load balancing reads host load, so it needs thread scope rather than diagnostics scope.
110 [WS_METHODS.serverGetHostResources]: AuthOrchestrationReadScope,
111 [WS_METHODS.serverGetProcessResourceHistory]: AuthDiagnosticsReadScope,
112 [WS_METHODS.serverGetResourceTelemetryHistory]: AuthDiagnosticsReadScope,
113 [WS_METHODS.serverRetryResourceTelemetry]: AuthDiagnosticsReadScope,
114 [WS_METHODS.serverGetUsageSummary]: AuthDiagnosticsReadScope,
115 [WS_METHODS.serverRefreshUsageRates]: AuthDiagnosticsReadScope,
116 [WS_METHODS.serverSignalProcess]: AuthEnvironmentMaintainScope,
117 [WS_METHODS.serverReportClientActivity]: AuthOrchestrationReadScope,
118 [WS_METHODS.scheduledTasksSubscribe]: AuthOrchestrationReadScope,
119 [WS_METHODS.secretsAnswerRequest]: AuthOrchestrationOperateScope,
120 // Delivery logs hold request bodies, so they need the same scope as the URL.
121 [WS_METHODS.scheduledTasksListWebhookDeliveries]: AuthOrchestrationOperateScope,
122 [WS_METHODS.scheduledTasksGetWebhookDelivery]: AuthOrchestrationOperateScope,
123 [WS_METHODS.cloudGetRelayClientStatus]: AuthRelayReadScope,
124 [WS_METHODS.cloudInstallRelayClient]: AuthRelayWriteScope,
125 [WS_METHODS.pullRequestsList]: AuthOrchestrationReadScope,
126 [WS_METHODS.pullRequestsListStats]: AuthOrchestrationReadScope,
127 [WS_METHODS.pullRequestsFilterOptions]: AuthOrchestrationReadScope,
128 [WS_METHODS.pullRequestsSummary]: AuthOrchestrationReadScope,
129 [WS_METHODS.pullRequestsRouting]: AuthOrchestrationReadScope,
130 [WS_METHODS.pullRequestsRoutingIdentity]: AuthOrchestrationReadScope,
131 [WS_METHODS.pullRequestsStack]: AuthOrchestrationReadScope,
132 [WS_METHODS.pullRequestsLinkedThreads]: AuthOrchestrationReadScope,
133 [WS_METHODS.pullRequestsDetail]: AuthOrchestrationReadScope,
134 [WS_METHODS.pullRequestsPreview]: AuthOrchestrationReadScope,
135 [WS_METHODS.pullRequestsChecks]: AuthOrchestrationReadScope,
136 [WS_METHODS.pullRequestsActivity]: AuthOrchestrationReadScope,
137 [WS_METHODS.pullRequestsReviewGuide]: AuthOrchestrationReadScope,
138 [WS_METHODS.pullRequestsThreadComments]: AuthOrchestrationReadScope,
139 [WS_METHODS.pullRequestsDiffFileContents]: AuthOrchestrationReadScope,
140 [WS_METHODS.pullRequestsFilesViewed]: AuthOrchestrationReadScope,
141 [WS_METHODS.pullRequestsChangeBreakdown]: AuthOrchestrationReadScope,
142 [WS_METHODS.pullRequestsInvalidate]: AuthOrchestrationReadScope,
143 [WS_METHODS.pullRequestsReportState]: AuthOrchestrationReadScope,
144 [WS_METHODS.pullRequestsSubscribeRefreshes]: AuthOrchestrationReadScope,
145 [WS_METHODS.pullRequestsSubscribeLinkedThreadRefreshes]: AuthOrchestrationReadScope,
146 [WS_METHODS.pullRequestsReviewerCandidates]: AuthOrchestrationReadScope,
147 [WS_METHODS.pullRequestsLabelCandidates]: AuthOrchestrationReadScope,
148 [WS_METHODS.sourceControlLookupRepository]: AuthOrchestrationReadScope,
149 [WS_METHODS.sourceControlAppAccess]: AuthOrchestrationReadScope,
150 [WS_METHODS.sourceControlStarred]: AuthOrchestrationReadScope,
151 [WS_METHODS.subscribeProjectClones]: AuthOrchestrationReadScope,
152 [WS_METHODS.projectsListEntries]: AuthFilesystemReadScope,
153 [WS_METHODS.projectsListIgnoredFiles]: AuthFilesystemReadScope,
154 [WS_METHODS.projectsReadFile]: AuthFilesystemReadScope,
155 [WS_METHODS.projectsSearchContents]: AuthFilesystemReadScope,
156 [WS_METHODS.projectsSearchEntries]: AuthFilesystemReadScope,
157 [WS_METHODS.projectsWriteFile]: AuthFilesystemWriteScope,
158 [WS_METHODS.projectsEnsureScratch]: AuthOrchestrationOperateScope,
159 [WS_METHODS.projectsCreateNew]: AuthOrchestrationOperateScope,
160 [WS_METHODS.shellOpenInEditor]: AuthOrchestrationOperateScope,
161 [WS_METHODS.filesystemBrowse]: AuthFilesystemReadScope,
162 [WS_METHODS.filesystemGetMetadata]: AuthFilesystemReadScope,
163 [WS_METHODS.agentSessionsScan]: AuthOrchestrationReadScope,
164 [WS_METHODS.agentSessionsImport]: AuthOrchestrationOperateScope,
165 [WS_METHODS.assetsCreateUrl]: AuthOrchestrationReadScope,
166 [WS_METHODS.assetsPersistChatAttachments]: AuthOrchestrationOperateScope,
167 [WS_METHODS.attachmentsCreateUploadUrl]: AuthOrchestrationOperateScope,
168 [WS_METHODS.attachmentsDelete]: AuthOrchestrationOperateScope,
169 [WS_METHODS.providerUploadFeedback]: AuthOrchestrationOperateScope,
170 [WS_METHODS.mcpAppsCallTool]: AuthOrchestrationOperateScope,
171 [WS_METHODS.mcpAppsToolInfo]: AuthOrchestrationReadScope,
172 [WS_METHODS.mcpAppsReadResource]: AuthOrchestrationReadScope,
173 [WS_METHODS.mcpAppsUpdateModelContext]: AuthOrchestrationOperateScope,
174 [WS_METHODS.subscribeVcsStatus]: AuthOrchestrationReadScope,
175 [WS_METHODS.subscribeVcsBranch]: AuthOrchestrationReadScope,
176 [WS_METHODS.vcsRefreshBranch]: AuthOrchestrationReadScope,
177 [WS_METHODS.subscribeWorktreeSetup]: AuthOrchestrationReadScope,
178 [WS_METHODS.worktreeSetupCancel]: AuthOrchestrationOperateScope,
179 [WS_METHODS.subscribeResourceTelemetry]: AuthDiagnosticsReadScope,
180 [WS_METHODS.vcsRefreshStatus]: AuthOrchestrationReadScope,
181 [WS_METHODS.gitResolvePullRequest]: AuthOrchestrationReadScope,
182 [WS_METHODS.vcsListRefs]: AuthOrchestrationReadScope,
183 [WS_METHODS.reviewGetBranchCommits]: AuthFilesystemReadScope,
184 [WS_METHODS.reviewGetDiffPreview]: AuthFilesystemReadScope,
185 [WS_METHODS.reviewGetDiffFileContents]: AuthFilesystemReadScope,
186 [WS_METHODS.terminalOpen]: AuthTerminalOperateScope,
187 [WS_METHODS.terminalAttach]: AuthTerminalOperateScope,
188 [WS_METHODS.terminalObserve]: AuthTerminalReadScope,
189 [WS_METHODS.terminalWrite]: AuthTerminalOperateScope,
190 [WS_METHODS.terminalResize]: AuthTerminalOperateScope,
191 [WS_METHODS.terminalClear]: AuthTerminalOperateScope,
192 [WS_METHODS.terminalClose]: AuthTerminalOperateScope,
193 [WS_METHODS.subscribeTerminalMetadata]: AuthTerminalReadScope,
194 [WS_METHODS.previewOpen]: AuthPreviewOperateScope,
195 [WS_METHODS.previewResize]: AuthPreviewOperateScope,
196 [WS_METHODS.previewAdjust]: AuthPreviewOperateScope,
197 [WS_METHODS.previewClose]: AuthPreviewOperateScope,
198 [WS_METHODS.previewList]: AuthOrchestrationReadScope,
199 [WS_METHODS.previewClearProfile]: AuthPreviewOperateScope,
200 [WS_METHODS.previewReportProfiles]: AuthPreviewOperateScope,
201 [WS_METHODS.previewReportStatus]: AuthPreviewOperateScope,
202 [WS_METHODS.previewStopLocalServer]: AuthTerminalOperateScope,
203 [WS_METHODS.subscribePreviewEvents]: AuthOrchestrationReadScope,
204 [WS_METHODS.subscribeDiscoveredLocalServers]: AuthOrchestrationReadScope,
205 [WS_METHODS.deviceConfigure]: AuthSettingsWriteScope,
206 [WS_METHODS.deviceTestHost]: AuthSettingsWriteScope,
207 [WS_METHODS.deviceEasAccount]: AuthSettingsWriteScope,
208 [WS_METHODS.deviceSimlockIdentity]: AuthSettingsWriteScope,
209 [WS_METHODS.deviceList]: AuthOrchestrationReadScope,
210 [WS_METHODS.deviceOpen]: AuthOrchestrationOperateScope,
211 [WS_METHODS.deviceClose]: AuthOrchestrationOperateScope,
212 [WS_METHODS.deviceShutdown]: AuthOrchestrationOperateScope,
213 [WS_METHODS.deviceDetail]: AuthOrchestrationReadScope,
214 [WS_METHODS.deviceAction]: AuthOrchestrationOperateScope,
215 [WS_METHODS.subscribeDeviceState]: AuthOrchestrationReadScope,
216 [WS_METHODS.subscribeServerConfig]: AuthOrchestrationReadScope,
217 [WS_METHODS.subscribeServerLifecycle]: AuthOrchestrationReadScope,
218 [WS_METHODS.subscribeAuthAccess]: AuthAccessReadScope,
219} as const satisfies Readonly<Record<WsRpcMethod, AuthEnvironmentScope>>;
220​
221export function requiredScopeForRpcMethod(method: string): AuthEnvironmentScope {
222 if (!Object.hasOwn(RPC_REQUIRED_SCOPES, method)) {
223 throw new Error(`RPC method ${method} has no declared authorization scope.`);
224 }
225 const requiredScope = RPC_REQUIRED_SCOPES[method as WsRpcMethod];
226 if (requiredScope === undefined) {
227 throw new Error(`RPC method ${method} has no declared authorization scope.`);
228 }
229 return requiredScope;
231​
232export const rpcAuthorizationError = (requiredScope: AuthEnvironmentScope) =>
233 new EnvironmentAuthorizationError({
234 message: `The authenticated token is missing required scope: ${requiredScope}.`,
235 ...authScopeRequiredResponse(requiredScope),
236 });
237​
238const SettingsUpdate = Schema.Struct({
239 patch: ServerSettingsPatch,
240 providerInstanceMutation: Schema.optionalKey(ProviderInstanceMutation),
241});
242// Middleware receives the payload already decoded, so it is checked against the decoded shape.
243const decodeSettingsUpdate = Schema.decodeUnknownSync(Schema.toType(SettingsUpdate));
244​
245const requiredScopesForSettingsUpdate = (payload: unknown) => {
246 const input = decodeSettingsUpdate(payload);
247 const scopes = requiredScopesForServerSettingsPatch(input.patch);
248 if (input.providerInstanceMutation === undefined) return scopes;
249 return Object.values(input.patch).every((value) => value === undefined)
250 ? [AuthProvidersManageScope]
251 : [...new Set([...scopes, AuthProvidersManageScope])];
252};
253​
254const decodeProjectMutation = Schema.decodeUnknownSync(ProjectMutation);
255const decodeAssetCreateUrlInput = Schema.decodeUnknownSync(AssetCreateUrlInput);
256​
257const requiredScopesForRpcCall = (
258 method: string,
259 payload: unknown,
260): ReadonlyArray<AuthEnvironmentScope> => {
261 if (method === WS_METHODS.serverRetryResourceTelemetry) {
262 return [AuthEnvironmentMaintainScope, AuthDiagnosticsReadScope];
263 }
264 if (method === WS_METHODS.assetsCreateUrl) {
265 const { resource } = decodeAssetCreateUrlInput(payload);
266 return [
267 resource._tag === "workspace-file" ||
268 resource._tag === "media-file" ||
269 resource._tag === "draft-workspace-file" ||
270 (resource._tag === "native-app-icon" && resource.app._tag === "app-path")
271 ? AuthFilesystemReadScope
272 : AuthOrchestrationReadScope,
273 ];
274 }
275 if (method === WS_METHODS.serverUpdateSettings) return requiredScopesForSettingsUpdate(payload);
276 if (method === WS_METHODS.projectsMutate) {
277 return requiredScopesForProjectMutation(decodeProjectMutation(payload));
278 }
279 const guarded = clientRpcRequiredScopes(method, payload);
280 if (guarded.length > 0) return guarded;
281 return [requiredScopeForRpcMethod(method)];
282};
283​
284/**
285 * `onAuthorized` runs once after an authorized request succeeds; for a stream
286 * RPC, that is when the stream completes. Every RPC passes through here, so it
287 * is the one place to observe requests without wrapping each handler.
288 */
289export const layer = (
290 scopes: ReadonlyArray<AuthEnvironmentScope>,
291 onAuthorized?: (method: string, payload: unknown) => Effect.Effect<void>,
292) =>
293 Layer.succeed(RpcScopeAuthorization)((effect, { rpc, payload }) => {
294 const requiredScopes = requiredScopesForRpcCall(rpc._tag, payload);
295 const requiredScope = requiredScopes.find((scope) => !scopes.includes(scope));
296 if (requiredScope !== undefined) return Effect.fail(rpcAuthorizationError(requiredScope));
297 return onAuthorized === undefined
298 ? effect
299 : Effect.tap(effect, () => onAuthorized(rpc._tag, payload));
300 });
301​
302/** Retrying can install or restart tools even though ordinary listing is readable. */
303export const requiredScopeForDeviceList = (input: DeviceListInput): AuthEnvironmentScope =>
304 input.retryHostId || input.updateTool
305 ? AuthOrchestrationOperateScope
306 : AuthOrchestrationReadScope;