Unified · Split
| @@ -89,13 +89,14 @@ export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed( · 88 unmodified lines | |||
| 89 | */ | 89 | */ |
| 90 | export const CLOUD_MOUNT_PREFIX = "/api" as const; | 90 | export const CLOUD_MOUNT_PREFIX = "/api" as const; |
| 91 | 91 | ||
| 92 | // Initial Google launch boundary. Calendar + Sheets are sensitive scopes but | 92 | // Initial Google launch boundary. Gmail uses gmail.modify for read, send, and |
| 93 | // not restricted Workspace scopes; Gmail and account-wide Drive remain absent | 93 | // trash operations while immediate permanent deletion remains absent until the |
| 94 | // until their separate verification/security work is complete. The same scope | 94 | // broader mail.google.com scope is approved. Account-wide Drive remains absent. |
| 95 | // source builds the catalog auth templates, preventing config drift. | 95 | // The same scope source builds the catalog auth templates, preventing drift. |
| 96 | const GOOGLE_FIRST_PARTY_ALLOWED_SCOPES: readonly string[] = [ | 96 | const GOOGLE_FIRST_PARTY_ALLOWED_SCOPES: readonly string[] = [ |
| 97 | ...new Set([ | 97 | ...new Set([ |
| 98 | ...googleCatalogOAuthScopesForPreset("google-calendar"), | 98 | ...googleCatalogOAuthScopesForPreset("google-calendar"), |
| 99 | ...googleCatalogOAuthScopesForPreset("google-gmail"), | ||
| 99 | ...googleCatalogOAuthScopesForPreset("google-sheets"), | 100 | ...googleCatalogOAuthScopesForPreset("google-sheets"), |
| 100 | ]), | 101 | ]), |
| 101 | ]; | 102 | ]; |
| @@ -133,14 +133,15 @@ Source (and the place to start if something breaks): https://github.com/UsefulSo · 132 unmodified lines | |||
| 133 | <h1 | 133 | <h1 |
| 134 | class="rise-1 text-[clamp(2.8rem,7.2vw,6rem)] font-semibold tracking-[-0.025em] leading-[1.02] text-ink mb-6 mx-auto max-w-[18ch] text-balance" | 134 | class="rise-1 text-[clamp(2.8rem,7.2vw,6rem)] font-semibold tracking-[-0.025em] leading-[1.02] text-ink mb-6 mx-auto max-w-[18ch] text-balance" |
| 135 | > | 135 | > |
| 136 | Connect any agent to <span class="serif-italic">everything</span>. | 136 | Executor connects any agent to <span class="serif-italic">everything</span>. |
| 137 | </h1> | 137 | </h1> |
| 138 | <p | 138 | <p |
| 139 | class="rise-2 mx-auto mb-12 max-w-[50ch] text-[clamp(1.05rem,2.4vw,1.3rem)] leading-[1.5] text-ink-2" | 139 | class="rise-2 mx-auto mb-12 max-w-[50ch] text-[clamp(1.05rem,2.4vw,1.3rem)] leading-[1.5] text-ink-2" |
| 140 | > | 140 | > |
| 141 | Executor is an MCP gateway. Anything that speaks MCP, like Claude | 141 | Executor is an integration platform and MCP gateway for AI agents. |
| 142 | Code, Cursor, or Codex, points at one endpoint and reaches every | 142 | It lets you securely connect services such as Google Workspace, |
| 143 | tool you connect. | 143 | GitHub, and Slack, then read data and run the actions you request |
| 144 | through one endpoint. | ||
| 144 | </p> | 145 | </p> |
| 145 | <div class="rise-3 max-w-[760px] mx-auto mb-12"> | 146 | <div class="rise-3 max-w-[760px] mx-auto mb-12"> |
| 146 | <div class="surface-card p-6 sm:p-7"> | 147 | <div class="surface-card p-6 sm:p-7"> |
| @@ -57,10 +57,15 @@ import LegalLayout from "../components/LegalLayout.astro"; · 56 unmodified lines | |||
| 57 | <p> | 57 | <p> |
| 58 | If you connect a Google Workspace service, Executor uses the permissions you grant to perform the actions you | 58 | If you connect a Google Workspace service, Executor uses the permissions you grant to perform the actions you |
| 59 | request through that integration. Depending on the service and permissions you choose, this may include accessing | 59 | request through that integration. Depending on the service and permissions you choose, this may include accessing |
| 60 | or modifying Google Calendar events, Google Sheets spreadsheets, or other Google Workspace content. Executor stores | 60 | or modifying Google Calendar events, Google Sheets spreadsheets, Gmail messages, drafts, threads, attachments, |
| 61 | OAuth credentials and connection metadata so the integration can continue working, and processes Google Workspace | 61 | labels, or other Google Workspace content. For Gmail, this can include reading and searching email, composing and |
| 62 | content when carrying out your requested tool calls and returning their results to you or the agent you directed to | 62 | sending messages, and organizing or moving messages to Trash when you request those actions. |
| 63 | make the call. | 63 | </p> |
| 64 | <p> | ||
| 65 | Executor stores OAuth credentials and connection metadata so the integration can continue working. It processes | ||
| 66 | Google Workspace content only while carrying out your requested tool calls and returning their results to you or | ||
| 67 | the agent you directed to make the call. That content can appear in the resulting agent session or in another | ||
| 68 | service when you explicitly direct Executor to send it there. | ||
| 64 | </p> | 69 | </p> |
| 65 | <p> | 70 | <p> |
| 66 | We do not use Google Workspace API data for advertising or to train generalized artificial-intelligence or | 71 | We do not use Google Workspace API data for advertising or to train generalized artificial-intelligence or |
| @@ -72,6 +77,11 @@ import LegalLayout from "../components/LegalLayout.astro"; · 5 unmodified lines | |||
| 72 | <a href="https://developers.google.com/terms/api-services-user-data-policy">Google API Services User Data Policy</a>, | 77 | <a href="https://developers.google.com/terms/api-services-user-data-policy">Google API Services User Data Policy</a>, |
| 73 | including its Limited Use requirements. | 78 | including its Limited Use requirements. |
| 74 | </p> | 79 | </p> |
| 80 | <p> | ||
| 81 | You can stop Executor's access by deleting the Google connection in Executor or revoking Executor from your | ||
| 82 | Google Account permissions. To request deletion of your Executor account, stored OAuth credentials, connection | ||
| 83 | metadata, or other personal information, email <a href="mailto:rhys@executor.sh">rhys@executor.sh</a>. | ||
| 84 | </p> | ||
| 75 | 85 | ||
| 76 | <h3>Website inputs and support communications</h3> | 86 | <h3>Website inputs and support communications</h3> |
| 77 | <p> | 87 | <p> |
| @@ -168,7 +168,7 @@ scenario( · 167 unmodified lines | |||
| 168 | ); | 168 | ); |
| 169 | 169 | ||
| 170 | scenario( | 170 | scenario( |
| 171 | "First-party OAuth · Google offers Calendar and Sheets but refuses Gmail scopes", | 171 | "First-party OAuth · Google offers Gmail modify but refuses full Gmail and Drive scopes", |
| 172 | {}, | 172 | {}, |
| 173 | Effect.scoped( | 173 | Effect.scoped( |
| 174 | Effect.gen(function* () { | 174 | Effect.gen(function* () { |
| @@ -184,6 +184,7 @@ scenario( · 9 unmodified lines | |||
| 184 | expect(google?.origin.kind).toBe("first_party"); | 184 | expect(google?.origin.kind).toBe("first_party"); |
| 185 | if (google?.origin.kind !== "first_party") return; | 185 | if (google?.origin.kind !== "first_party") return; |
| 186 | expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/calendar"); | 186 | expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/calendar"); |
| 187 | expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/gmail.modify"); | ||
| 187 | expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/spreadsheets"); | 188 | expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/spreadsheets"); |
| 188 | expect(google.origin.allowedScopes).not.toContain("https://mail.google.com/"); | 189 | expect(google.origin.allowedScopes).not.toContain("https://mail.google.com/"); |
| 189 | expect(google.origin.allowedScopes).not.toContain("https://www.googleapis.com/auth/drive"); | 190 | expect(google.origin.allowedScopes).not.toContain("https://www.googleapis.com/auth/drive"); |
| @@ -229,19 +230,50 @@ scenario( · 39 unmodified lines | |||
| 229 | "openid", | 230 | "openid", |
| 230 | "email", | 231 | "email", |
| 231 | "profile", | 232 | "profile", |
| 232 | "https://mail.google.com/", | 233 | "https://www.googleapis.com/auth/gmail.modify", |
| 233 | ]), | 234 | ]), |
| 234 | slug: gmail, | 235 | slug: gmail, |
| 235 | }, | 236 | }, |
| 236 | }); | 237 | }); |
| 238 | const gmailStarted = yield* client.oauth.start({ | ||
| 239 | payload: { | ||
| 240 | client: OAuthClientSlug.make("first-party:google"), | ||
| 241 | clientOwner: "org", | ||
| 242 | owner: "org", | ||
| 243 | name: ConnectionName.make("gmail"), | ||
| 244 | integration: gmail, | ||
| 245 | template: AuthTemplateSlug.make("oauth"), | ||
| 246 | }, | ||
| 247 | }); | ||
| 248 | expect(gmailStarted.status).toBe("redirect"); | ||
| 249 | const gmailAuthorizationUrl = | ||
| 250 | gmailStarted.status === "redirect" ? gmailStarted.authorizationUrl : ""; | ||
| 251 | expect( | ||
| 252 | new Set(new URL(gmailAuthorizationUrl).searchParams.get("scope")?.split(" ") ?? []), | ||
| 253 | ).toEqual( | ||
| 254 | new Set(["openid", "email", "profile", "https://www.googleapis.com/auth/gmail.modify"]), | ||
| 255 | ); | ||
| 256 | |||
| 257 | const fullGmail = IntegrationSlug.make(unique("google_gmail_full")); | ||
| 258 | yield* client.openapi.addSpec({ | ||
| 259 | payload: { | ||
| 260 | ...googleShapedIntegrationSpec([ | ||
| 261 | "openid", | ||
| 262 | "email", | ||
| 263 | "profile", | ||
| 264 | "https://mail.google.com/", | ||
| 265 | ]), | ||
| 266 | slug: fullGmail, | ||
| 267 | }, | ||
| 268 | }); | ||
| 237 | const blocked = yield* client.oauth | 269 | const blocked = yield* client.oauth |
| 238 | .start({ | 270 | .start({ |
| 239 | payload: { | 271 | payload: { |
| 240 | client: OAuthClientSlug.make("first-party:google"), | 272 | client: OAuthClientSlug.make("first-party:google"), |
| 241 | clientOwner: "org", | 273 | clientOwner: "org", |
| 242 | owner: "org", | 274 | owner: "org", |
| 243 | name: ConnectionName.make("gmail"), | 275 | name: ConnectionName.make("gmail-full"), |
| 244 | integration: gmail, | 276 | integration: fullGmail, |
| 245 | template: AuthTemplateSlug.make("oauth"), | 277 | template: AuthTemplateSlug.make("oauth"), |
| 246 | }, | 278 | }, |
| 247 | }) | 279 | }) |
| @@ -959,6 +959,78 @@ it.effect("bundles Google Discovery documents into one Google OpenAPI integratio · 958 unmodified lines | |||
| 959 | }), | 959 | }), |
| 960 | ); | 960 | ); |
| 961 | 961 | ||
| 962 | it.effect("filters Gmail operations to the explicitly selected consent scope", () => | ||
| 963 | Effect.gen(function* () { | ||
| 964 | const modifyScope = "https://www.googleapis.com/auth/gmail.modify"; | ||
| 965 | const fullScope = "https://mail.google.com/"; | ||
| 966 | const result = yield* convertGoogleDiscoveryBundleToOpenApi({ | ||
| 967 | consentScopes: [modifyScope], | ||
| 968 | documents: [ | ||
| 969 | { | ||
| 970 | discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest", | ||
| 971 | // @effect-diagnostics-next-line preferSchemaOverJson:off | ||
| 972 | documentText: JSON.stringify({ | ||
| 973 | name: "gmail", | ||
| 974 | version: "v1", | ||
| 975 | title: "Gmail API", | ||
| 976 | rootUrl: "https://gmail.googleapis.com/", | ||
| 977 | servicePath: "", | ||
| 978 | auth: { | ||
| 979 | oauth2: { | ||
| 980 | scopes: { | ||
| 981 | [modifyScope]: { description: "Read and modify Gmail" }, | ||
| 982 | [fullScope]: { description: "Full Gmail access" }, | ||
| 983 | }, | ||
| 984 | }, | ||
| 985 | }, | ||
| 986 | resources: { | ||
| 987 | users: { | ||
| 988 | resources: { | ||
| 989 | messages: { | ||
| 990 | methods: { | ||
| 991 | list: { | ||
| 992 | id: "gmail.users.messages.list", | ||
| 993 | httpMethod: "GET", | ||
| 994 | path: "gmail/v1/users/{userId}/messages", | ||
| 995 | scopes: [modifyScope, fullScope], | ||
| 996 | parameters: { | ||
| 997 | userId: { location: "path", required: true, type: "string" }, | ||
| 998 | }, | ||
| 999 | }, | ||
| 1000 | delete: { | ||
| 1001 | id: "gmail.users.messages.delete", | ||
| 1002 | httpMethod: "DELETE", | ||
| 1003 | path: "gmail/v1/users/{userId}/messages/{id}", | ||
| 1004 | scopes: [fullScope], | ||
| 1005 | parameters: { | ||
| 1006 | userId: { location: "path", required: true, type: "string" }, | ||
| 1007 | id: { location: "path", required: true, type: "string" }, | ||
| 1008 | }, | ||
| 1009 | }, | ||
| 1010 | }, | ||
| 1011 | }, | ||
| 1012 | }, | ||
| 1013 | }, | ||
| 1014 | }, | ||
| 1015 | schemas: {}, | ||
| 1016 | }), | ||
| 1017 | }, | ||
| 1018 | ], | ||
| 1019 | }); | ||
| 1020 | |||
| 1021 | const spec = decodeConvertedSpec(result.specText); | ||
| 1022 | const operationIds = Object.values(spec.paths).flatMap((path) => | ||
| 1023 | Object.values(path).map((operation) => operation.operationId), | ||
| 1024 | ); | ||
| 1025 | expect(operationIds).toContain("gmail.users.messages.list"); | ||
| 1026 | expect(operationIds).not.toContain("gmail.users.messages.delete"); | ||
| 1027 | const oauthTemplate = result.authenticationTemplate?.find((entry) => entry.kind === "oauth2"); | ||
| 1028 | expect(oauthTemplate?.kind === "oauth2" ? oauthTemplate.scopes : undefined).toEqual([ | ||
| 1029 | modifyScope, | ||
| 1030 | ]); | ||
| 1031 | }), | ||
| 1032 | ); | ||
| 1033 | |||
| 962 | // --------------------------------------------------------------------------- | 1034 | // --------------------------------------------------------------------------- |
| 963 | // The merged bundle scope set is the COMPACTED + FILTERED union: sub-scopes | 1035 | // The merged bundle scope set is the COMPACTED + FILTERED union: sub-scopes |
| 964 | // collapse under their broad parent (`gmail.*` → `mail.google.com/`, | 1036 | // collapse under their broad parent (`gmail.*` → `mail.google.com/`, |
| @@ -831,9 +831,6 @@ const GOOGLE_PHOTOS_APPENDONLY_SCOPE = "https://www.googleapis.com/auth/photosli · 830 unmodified lines | |||
| 831 | const GOOGLE_PHOTOS_UPLOAD_TOOL_PATH = "photoslibrary.mediaItems.upload"; | 831 | const GOOGLE_PHOTOS_UPLOAD_TOOL_PATH = "photoslibrary.mediaItems.upload"; |
| 832 | const GOOGLE_PHOTOS_UPLOAD_PATH = "/v1/uploads"; | 832 | const GOOGLE_PHOTOS_UPLOAD_PATH = "/v1/uploads"; |
| 833 | 833 | ||
| 834 | const isGooglePhotosService = (service: string): boolean => | ||
| 835 | service === GOOGLE_PHOTOS_LIBRARY_SERVICE || service === GOOGLE_PHOTOS_PICKER_SERVICE; | ||
| 836 | |||
| 837 | const discoveryScopesForService = ( | 834 | const discoveryScopesForService = ( |
| 838 | service: string, | 835 | service: string, |
| 839 | document: DiscoveryDocument, | 836 | document: DiscoveryDocument, |
| @@ -1124,10 +1121,10 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn( · 284 unmodified lines | |||
| 1124 | const schemaPrefix = schemaComponentPart(`${info.service}_${info.version}`); | 1121 | const schemaPrefix = schemaComponentPart(`${info.service}_${info.version}`); |
| 1125 | const schemaNameForRef = (name: string) => `${schemaPrefix}_${schemaComponentPart(name)}`; | 1122 | const schemaNameForRef = (name: string) => `${schemaPrefix}_${schemaComponentPart(name)}`; |
| 1126 | const scopeDescriptions = discoveryScopesForService(info.service, info.document); | 1123 | const scopeDescriptions = discoveryScopesForService(info.service, info.document); |
| 1127 | const filterPhotosScopes = consentScopeSet !== null && isGooglePhotosService(info.service); | 1124 | const filterConsentScopes = consentScopeSet !== null; |
| 1128 | 1125 | ||
| 1129 | for (const [scope, description] of Object.entries(scopeDescriptions)) { | 1126 | for (const [scope, description] of Object.entries(scopeDescriptions)) { |
| 1130 | if (filterPhotosScopes && !consentScopeSet.has(scope)) continue; | 1127 | if (filterConsentScopes && !consentScopeSet.has(scope)) continue; |
| 1131 | rawScopes[scope] ??= description; | 1128 | rawScopes[scope] ??= description; |
| 1132 | } | 1129 | } |
| 1133 | 1130 | ||
| @@ -1140,10 +1137,10 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn( · 6 unmodified lines | |||
| 1140 | const rawPathTemplate = Option.getOrUndefined(method.path); | 1137 | const rawPathTemplate = Option.getOrUndefined(method.path); |
| 1141 | if (!methodId || !rawPathTemplate || !method.httpMethod) continue; | 1138 | if (!methodId || !rawPathTemplate || !method.httpMethod) continue; |
| 1142 | const methodScopes = discoveryMethodScopesForService(info.service, method); | 1139 | const methodScopes = discoveryMethodScopesForService(info.service, method); |
| 1143 | const oauthScopes = filterPhotosScopes | 1140 | const oauthScopes = filterConsentScopes |
| 1144 | ? methodScopes.filter((scope) => consentScopeSet.has(scope)) | 1141 | ? methodScopes.filter((scope) => consentScopeSet.has(scope)) |
| 1145 | : methodScopes; | 1142 | : methodScopes; |
| 1146 | if (filterPhotosScopes && methodScopes.length > 0 && oauthScopes.length === 0) continue; | 1143 | if (filterConsentScopes && methodScopes.length > 0 && oauthScopes.length === 0) continue; |
| 1147 | 1144 | ||
| 1148 | const toolPath = methodId; | 1145 | const toolPath = methodId; |
| 1149 | const wirePath = rawPathTemplate.startsWith("/") ? rawPathTemplate : `/${rawPathTemplate}`; | 1146 | const wirePath = rawPathTemplate.startsWith("/") ? rawPathTemplate : `/${rawPathTemplate}`; |
| @@ -250,7 +250,7 @@ export const googlePhotosOpenApiPresets: readonly GoogleOpenApiPreset[] = · 249 unmodified lines | |||
| 250 | 250 | ||
| 251 | export const googleOAuthConsentScopes: Readonly<Record<string, readonly string[]>> = { | 251 | export const googleOAuthConsentScopes: Readonly<Record<string, readonly string[]>> = { |
| 252 | "google-calendar": ["https://www.googleapis.com/auth/calendar"], | 252 | "google-calendar": ["https://www.googleapis.com/auth/calendar"], |
| 253 | "google-gmail": ["https://mail.google.com/"], | 253 | "google-gmail": ["https://www.googleapis.com/auth/gmail.modify"], |
| 254 | "google-sheets": ["https://www.googleapis.com/auth/spreadsheets"], | 254 | "google-sheets": ["https://www.googleapis.com/auth/spreadsheets"], |
| 255 | "google-drive": ["https://www.googleapis.com/auth/drive"], | 255 | "google-drive": ["https://www.googleapis.com/auth/drive"], |
| 256 | "google-docs": ["https://www.googleapis.com/auth/documents"], | 256 | "google-docs": ["https://www.googleapis.com/auth/documents"], |
| @@ -61,7 +61,10 @@ export const googleDiscoveryAdapter: SpecFormatAdapter = { · 60 unmodified lines | |||
| 61 | ), | 61 | ), |
| 62 | { concurrency: 4 }, | 62 | { concurrency: 4 }, |
| 63 | ); | 63 | ); |
| 64 | const conversion = yield* convertGoogleDiscoveryBundleToOpenApi({ documents }); | 64 | const conversion = yield* convertGoogleDiscoveryBundleToOpenApi({ |
| 65 | documents, | ||
| 66 | ...(input.consentScopes ? { consentScopes: input.consentScopes } : {}), | ||
| 67 | }); | ||
| 65 | const document = | 68 | const document = |
| 66 | documents.length === 1 | 69 | documents.length === 1 |
| 67 | ? yield* parseJson(documents[0]!.documentText) | 70 | ? yield* parseJson(documents[0]!.documentText) |
| @@ -646,7 +646,13 @@ export const openApiPlugin = definePlugin< · 645 unmodified lines | |||
| 646 | const resolveSpecForInput = ( | 646 | const resolveSpecForInput = ( |
| 647 | config: Pick< | 647 | config: Pick< |
| 648 | OpenApiSpecConfig, | 648 | OpenApiSpecConfig, |
| 649 | "spec" | "specFormat" | "specOverrides" | "headers" | "queryParams" | "baseUrl" | 649 | | "spec" |
| 650 | | "specFormat" | ||
| 651 | | "specOverrides" | ||
| 652 | | "headers" | ||
| 653 | | "queryParams" | ||
| 654 | | "baseUrl" | ||
| 655 | | "authenticationTemplate" | ||
| 650 | >, | 656 | >, |
| 651 | httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>, | 657 | httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>, |
| 652 | ): Effect.Effect< | 658 | ): Effect.Effect< |
| @@ -670,6 +676,13 @@ export const openApiPlugin = definePlugin< · 17 unmodified lines | |||
| 670 | ...(config.headers ? { headers: config.headers } : {}), | 676 | ...(config.headers ? { headers: config.headers } : {}), |
| 671 | ...(config.queryParams ? { queryParams: config.queryParams } : {}), | 677 | ...(config.queryParams ? { queryParams: config.queryParams } : {}), |
| 672 | }, | 678 | }, |
| 679 | ...(config.authenticationTemplate | ||
| 680 | ? { | ||
| 681 | consentScopes: config.authenticationTemplate.flatMap((template) => | ||
| 682 | "kind" in template && template.kind === "oauth2" ? template.scopes : [], | ||
| 683 | ), | ||
| 684 | } | ||
| 685 | : {}), | ||
| 673 | httpClientLayer, | 686 | httpClientLayer, |
| 674 | }); | 687 | }); |
| 675 | return yield* applyOverridesToResolvedSpec(resolved, config.specOverrides); | 688 | return yield* applyOverridesToResolvedSpec(resolved, config.specOverrides); |
| @@ -15,6 +15,9 @@ export interface SpecFetchCredentials { · 14 unmodified lines | |||
| 15 | export interface SpecFetchInput { | 15 | export interface SpecFetchInput { |
| 16 | readonly urls: readonly string[]; | 16 | readonly urls: readonly string[]; |
| 17 | readonly credentials?: SpecFetchCredentials; | 17 | readonly credentials?: SpecFetchCredentials; |
| 18 | /** Explicit OAuth scopes selected by the caller. Format adapters may use | ||
| 19 | * these to omit operations that the resulting connection cannot invoke. */ | ||
| 20 | readonly consentScopes?: readonly string[]; | ||
| 18 | readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>; | 21 | readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>; |
| 19 | } | 22 | } |
| 20 | 23 | ||
| @@ -1565,6 +1565,9 @@ function AddAccountModalView(props: AddAccountModalProps) { · 1564 unmodified lines | |||
| 1565 | [...oauthApps, ...oauthNearApps, ...oauthOtherApps].find( | 1565 | [...oauthApps, ...oauthNearApps, ...oauthOtherApps].find( |
| 1566 | (c: OAuthClientOption) => String(c.slug) === selectedApp, | 1566 | (c: OAuthClientOption) => String(c.slug) === selectedApp, |
| 1567 | ) ?? null; | 1567 | ) ?? null; |
| 1568 | const isBuiltInGoogleClient = | ||
| 1569 | chosenClient?.origin.kind === "first_party" && | ||
| 1570 | String(chosenClient.slug) === "first-party:google"; | ||
| 1568 | const oauthBusy = ccBusy || oauthPopup.busy; | 1571 | const oauthBusy = ccBusy || oauthPopup.busy; |
| 1569 | const cimdConnecting = cimdBusy || oauthPopup.busy; | 1572 | const cimdConnecting = cimdBusy || oauthPopup.busy; |
| 1570 | const dcrConnecting = dcrBusy || oauthPopup.busy; | 1573 | const dcrConnecting = dcrBusy || oauthPopup.busy; |
| @@ -2880,6 +2883,22 @@ function AddAccountModalView(props: AddAccountModalProps) { · 1309 unmodified lines | |||
| 2880 | /> | 2883 | /> |
| 2881 | </div> | 2884 | </div> |
| 2882 | )} | 2885 | )} |
| 2886 | |||
| 2887 | {isBuiltInGoogleClient && showPlaceStep ? ( | ||
| 2888 | <p className="rounded-md border border-border/60 bg-muted/20 px-3 py-2 text-xs text-muted-foreground"> | ||
| 2889 | Executor uses the Google data you authorize only to perform the actions you | ||
| 2890 | request. Read the{` `} | ||
| 2891 | <a | ||
| 2892 | href="https://executor.sh/privacy" | ||
| 2893 | target="_blank" | ||
| 2894 | rel="noreferrer" | ||
| 2895 | className="font-medium text-foreground underline underline-offset-2" | ||
| 2896 | > | ||
| 2897 | privacy policy | ||
| 2898 | </a> | ||
| 2899 | . | ||
| 2900 | </p> | ||
| 2901 | ) : null} | ||
| 2883 | </div> | 2902 | </div> |
| 2884 | 2903 | ||
| 2885 | {continueError ? ( | 2904 | {continueError ? ( |