Repository navigation
improvement(egress): route workspace requests through the organization's egress gateway - #10040
waleedlatif1 wants to merge 25 commits into
Conversation
…hrough the organization egress gateway
… providers, and runtime libs through outboundFetch Vendor API calls made for a workspace (tool post-processing, webhook subscription and polling, LLM provider SDK clients, embeddings, OCR, reranking, data drains, KB connectors and access checks, selectors, managed agents, media) now use outboundFetch so an organization with a dedicated egress gateway sends them from its static IPs. Sim-internal, operator-local (Ollama, vLLM/LiteLLM env endpoints), and platform calls stay on the global fetch. Slack Search deliveries (home publish, stop, message response, onboarding sender check) now run in the installation's organization scope; the public marketplace install exchange runs as a platform request.
…h the organization's gateway organizationAwsRequestHandler resolves the outbound route per request: a gateway route tunnels through a per-organization keep-alive agent (never shared across organizations or with direct traffic), a direct route keeps awsRequestHandler's behaviour, and a call without a scope goes direct with a warning. With routing disabled it is awsRequestHandler. Used for AWS tool clients (awsClientConfig, unauthenticated STS), saved-credential STS AssumeRole / AssumeRoleWithWebIdentity / GetCallerIdentity, and Bedrock on customer credentials. Sim's own AWS usage keeps awsRequestHandler. The gateway connect sequence is shared with createGatewayDispatcher via connectThroughGateway, and an OutboundRoutingError is reported as unavailable rather than as a rejected credential.
…organization-egress
…on gateway - refreshOAuthToken (incl. the Instagram long-lived refresh) and its provider config move to the server-only lib/oauth/refresh.server.ts, so the outboundFetch import never enters client bundles through lib/oauth/oauth.ts - vendor token exchange, refresh, userinfo, revoke, and discovery calls in lib/oauth, lib/auth/connectors, and lib/atlassian use outboundFetch - Instagram, Trello, and Shopify connect callbacks resolve their outbound scope from the caller's connect draft owner - the Teams subscription renewal cron scopes each subscription to its workflow's workspace organization - remove the unused /api/auth/oauth/token route, its contracts, and the delegation-header authenticator only it used
… reuse gateway sockets
…ors, and harden the audit
…stings, and v1 kb search embeddings through the workspace egress route Pass outboundFetch as the @google/genai httpOptions.fetch for the Google, Vertex, file-attachment, and vision clients. Run wand OpenAI calls, BYOK/hosted model catalog listings, and v1 knowledge search query embeddings inside the canonical workspace's outbound scope after access is verified.
…raft and report egress outages as 503 Generic OAuth connectors' custom getToken/getUserInfo now run in the outbound scope of the connect draft named by Better Auth's verified OAuth state, so an organization with a dedicated egress gateway connects from its own IPs. The Salesforce instance lookup in account.create.before uses the same draft scope. An OutboundRoutingError during OAuth refresh is no longer swallowed into a generic failed refresh: it propagates through the coalesced refresh without marking the credential dead or revoked, and token resolution maps it (and the service-account path) to 503 instead of 401.
…changes through the organization gateway Connectors without their own getToken now get a default one that performs Better Auth's generic OAuth exchange (authorizationCodeRequest, discovery-resolved token endpoint, basic/post client auth, PKCE, static tokenUrlParams, redirect refusal, getOAuth2Tokens) but sends it with outboundFetch inside the connect draft's scope. Credential-group managed OAuth exchanges the code the same way and reports an unavailable egress route as 503. A managed OAuth refresh that hits an unavailable egress route reports a retryable 503 refresh failure instead of an unhandled error, without marking the credential for reauthorization.
…y handlers, and simplify workspace fetches
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
[High impact] The routing change reaches outbound requests across many workspace workflows. Correct the routing of operator-owned Azure requests before merging. FindingsSummaryRoutes workspace-owned HTTP and customer AWS requests through organization gateways.
|
| ): Promise<ProviderResponse | StreamingExecution> { | ||
| const { logger } = config | ||
| const fetchImpl = config.fetch ?? fetch | ||
| const fetchImpl = config.fetch ?? outboundFetch |
There was a problem hiding this comment.
Azure requests take wrong route
azureOpenAIProvider also calls this helper with fetch: undefined when using env.AZURE_OPENAI_ENDPOINT. The new default sends those operator-owned Azure requests through the customer's gateway. If the deployment is private or restricted to Sim's IPs, model requests now fail.
Keep the global fetch for operator-configured Azure endpoints while retaining routing for customer endpoints. The repository requires Sim's own infrastructure to stay direct.
Context Used: CLAUDE.md (source)
| refreshed: result.refreshed, | ||
| } | ||
| } | ||
| if (result.outcome?.kind === 'egress_unavailable') throw result.outcome.error |
There was a problem hiding this comment.
Gateway outages become connection errors
The new egress_unavailable result is rethrown only by refreshTokenIfNeeded. Both getOAuthToken and resolveCredentialTokenBundle still turn every failed result into null. An expired Zoom token during a gateway outage therefore makes resolveSelectorOAuthAccessToken throw SelectorConnectionUnavailableError, which the selector route returns as 403.
Preserve the outage result through these callers too, so users see a retryable outage rather than a broken connection.
There was a problem hiding this comment.
17 issues found across 305 files
Confidence score: 2/5
- Fireflies, Google Drive, and GitHub knowledge requests can send requests directly when their outbound scope is missing, bypassing the fail-closed behavior. Use
scopedOutboundFetchinfireflies.ts,google-drive-errors.ts, andgithub-installation.ts. credential-service.tsturns egress outages into generic token failures, and refresh followers can see a 401 while the leader gets a 503. Propagateegress_unavailablethrough the bundle and token paths and make the outcome visible to followers.- The
OutboundRoutingErrorpassthrough inaws-service-account.server.tsis wrapped bybuildAwsServiceAccountSecret, so callers never receive it. Preserve the routing error throughservice-account-secret.ts. - Azure requests in
openai/core.tscan go through the active organization’s gateway when the caller leavesfetchunset. Pass globalfetchfor operator-configured Azure endpoints.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/sim/lib/webhooks/polling/google-drive.ts">
<violation number="1" location="apps/sim/lib/webhooks/polling/google-drive.ts:214">
P2: A gateway or routing outage now throws from `outboundFetch`, and this poller's generic catch counts it with `markWebhookFailed`. After enough consecutive polls the Drive trigger is auto-disabled, even though the source is fine. Consider classifying `OutboundRoutingError` in this catch, or treating it like other source-fetch failures.</violation>
</file>
<file name="apps/sim/lib/oauth/credential-service.ts">
<violation number="1" location="apps/sim/lib/oauth/credential-service.ts:1265">
P2: Egress outages on the bundle and token paths still collapse to `null`, so these callers report a generic missing-token failure, not a 503. Propagate `egress_unavailable` through `resolveCredentialTokenBundle` and `getOAuthToken` by throwing the stored error, as `refreshTokenIfNeeded` now does. Also log the egress failure before returning, because the early return skips the leader's `logger.error`.</violation>
<violation number="2" location="apps/sim/lib/oauth/credential-service.ts:1856">
P2: A follower that waits on another process's refresh gets a generic 401 during an egress outage, while the leader gets the 503. The leader's `egress_unavailable` outcome is not visible to followers, so the same outage maps to two different responses. Have the follower path surface the egress outage, or map a null follower result to the egress error when the lease holder failed on routing.</violation>
</file>
<file name="apps/sim/lib/oauth/salesforce-instance.server.ts">
<violation number="1" location="apps/sim/lib/oauth/salesforce-instance.server.ts:18">
P2: This `catch` swallows gateway outage errors, so an org egress outage on the Salesforce identity lookup surfaces as a 502 `IDENTITY_UNAVAILABLE` instead of the 503 this PR maps egress outages to. Rethrow `OutboundRoutingError` here so the caller can map it to 503. The caller in `managed-oauth.ts` also needs to handle it.</violation>
</file>
<file name="apps/sim/lib/auth/auth.ts">
<violation number="1" location="apps/sim/lib/auth/auth.ts:521">
P2: The Salesforce identity lookup is now scoped in `account.create.before`, but the identical lookup in `account.create.after` still runs with no outbound scope. For orgs with a dedicated gateway, that request leaves from Sim's global IPs. Wrap the after-hook call with the same draft-scope resolution, or reuse the instance URL the before hook already stored.</violation>
</file>
<file name="apps/sim/lib/knowledge/access/github-installation.ts">
<violation number="1" location="apps/sim/lib/knowledge/access/github-installation.ts:75">
P2: This knowledge-connector GitHub read should use `scopedOutboundFetch`, not `outboundFetch`. Without an organization scope, `outboundFetch` silently goes direct, so the repository-authorization check can bypass the org's egress gateway. `secure-fetch.server.ts` already uses the fail-closed variant for knowledge fetches. Import and call `scopedOutboundFetch` here.</violation>
</file>
<file name="apps/sim/lib/data-drains/destinations/gcs.ts">
<violation number="1" location="apps/sim/lib/data-drains/destinations/gcs.ts:143">
P2: The GCS access token is still minted by `google-auth-library` through `JWT.getAccessToken()`, which bypasses `outboundFetch` and goes direct. Only the upload and delete calls use the org gateway, so the credential exchange is not covered by the egress policy. Pass a transporter that routes through `outboundFetch` into `buildJwt`, or call out this exclusion explicitly.</violation>
</file>
<file name="apps/sim/lib/credentials/token-service-accounts/errors.ts">
<violation number="1" location="apps/sim/lib/credentials/token-service-accounts/errors.ts:228">
P2: For gateway-routed organizations, an unresolvable user-supplied host no longer maps to the caller's `dnsFailureCode`. `connectThroughGateway` turns the DNS failure into `ROUTE_BLOCKED`, so `fetchProvider` returns `provider_unavailable` (503 retry) instead of `site_not_found`. Preserve the resolver error (`ENOTFOUND`) through the gateway connect path, or check the DNS refusal before mapping to `ROUTE_BLOCKED`.</violation>
</file>
<file name="apps/sim/lib/credentials/databricks-service-account.server.ts">
<violation number="1" location="apps/sim/lib/credentials/databricks-service-account.server.ts:166">
P2: This catch turns a gateway outage from `outboundFetch` into a 502 that blames the Databricks workspace. Rethrow `OutboundRoutingError` (or map it to a 503 `provider_unavailable`) before the generic 502, matching `fetchProvider` in `token-service-accounts/errors.ts`.</violation>
</file>
<file name="apps/sim/lib/internal/telegram/operations.ts">
<violation number="1" location="apps/sim/lib/internal/telegram/operations.ts:104">
P3: A gateway outage raised by `outboundFetch` here propagates past `sendTelegramDocument`'s handlers and reaches `executeTelegramTool`, which returns 500 for it. The PR maps egress outages to 503, so map `OutboundRoutingError` (GATEWAY_UNAVAILABLE) to 503 in `execute-tool.ts`, or wrap this call.</violation>
</file>
<file name="apps/sim/lib/credentials/microsoft-app-service-account.server.ts">
<violation number="1" location="apps/sim/lib/credentials/microsoft-app-service-account.server.ts:418">
P2: An egress outage from `outboundFetch` is caught in `requestEntraToken` and rethrown as a 502 "Could not reach Microsoft Entra" error. The other egress handlers in this PR map `OutboundRoutingError` to 503. Rethrow `OutboundRoutingError` unchanged, or map it to 503, so it matches that contract.</violation>
</file>
<file name="apps/sim/lib/credentials/aws-service-account.server.ts">
<violation number="1" location="apps/sim/lib/credentials/aws-service-account.server.ts:285">
P2: The new `OutboundRoutingError` passthrough here never reaches the caller. `buildAwsServiceAccountSecret` in `service-account-secret.ts` wraps every error except `AwsCredentialError` as a `ServiceAccountSecretError`, and credential create maps that to a 400. The pin probe in `assertRolePinsSubject` has the same gap: it wraps the error into an `AwsCredentialError`. Let egress errors pass through both catches so they reach the 503 mapping.</violation>
</file>
<file name="apps/sim/lib/credential-groups/standard-oauth-provider.ts">
<violation number="1" location="apps/sim/lib/credential-groups/standard-oauth-provider.ts:319">
P2: The new 503 mapping covers only the code exchange. `managed.verifyIdentity` also performs `outboundFetch` calls, but its catch still converts gateway outages to 502; preserve `OutboundRoutingError` as a 503 there too.</violation>
</file>
<file name="apps/sim/connectors/fireflies/fireflies.ts">
<violation number="1" location="apps/sim/connectors/fireflies/fireflies.ts:204">
P1: Fireflies knowledge-sync requests use the fallback `outboundFetch`, allowing a missing scope to send the customer token directly. Use `scopedOutboundFetch` here so connector scope loss remains fail-closed.</violation>
</file>
<file name="apps/sim/app/api/wand/route.ts">
<violation number="1" location="apps/sim/app/api/wand/route.ts:374">
P2: Egress failures from `postWandRequest` are returned as HTTP 500, not 503. Map `OutboundRoutingError` gateway/configuration failures to 503 in both the streaming and non-streaming error paths so gateway outages remain retryable and are not treated as ordinary generation failures.</violation>
</file>
<file name="apps/sim/connectors/google-drive/google-drive-errors.ts">
<violation number="1" location="apps/sim/connectors/google-drive/google-drive-errors.ts:173">
P1: Google Drive knowledge requests now use the fallback `outboundFetch`, so a missing outbound scope is downgraded to a direct request instead of failing closed. Use `scopedOutboundFetch` for this connector path (or pass it explicitly) so scope loss cannot bypass organization egress.</violation>
</file>
<file name="apps/sim/providers/openai/core.ts">
<violation number="1" location="apps/sim/providers/openai/core.ts:160">
P2: Pass global `fetch` for operator-configured Azure endpoints; this default routes them through the active organization's gateway when the Azure caller leaves `fetch` unset.</violation>
</file>
Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.
Turn on auto-fix | Re-trigger cubic
| async () => { | ||
| /** One retry layer owns transport, HTTP, and GraphQL semantic failures. */ | ||
| const response = await fetch(FIREFLIES_GRAPHQL_URL, { | ||
| const response = await outboundFetch(FIREFLIES_GRAPHQL_URL, { |
There was a problem hiding this comment.
P1: Fireflies knowledge-sync requests use the fallback outboundFetch, allowing a missing scope to send the customer token directly. Use scopedOutboundFetch here so connector scope loss remains fail-closed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/connectors/fireflies/fireflies.ts, line 204:
<comment>Fireflies knowledge-sync requests use the fallback `outboundFetch`, allowing a missing scope to send the customer token directly. Use `scopedOutboundFetch` here so connector scope loss remains fail-closed.</comment>
<file context>
@@ -200,7 +201,7 @@ async function firefliesGraphQL(
async () => {
/** One retry layer owns transport, HTTP, and GraphQL semantic failures. */
- const response = await fetch(FIREFLIES_GRAPHQL_URL, {
+ const response = await outboundFetch(FIREFLIES_GRAPHQL_URL, {
method: 'POST',
headers: {
</file context>
| return retryWithExponentialBackoff( | ||
| async () => { | ||
| const response = await fetch(url, options) | ||
| const response = await outboundFetch(url, options) |
There was a problem hiding this comment.
P1: Google Drive knowledge requests now use the fallback outboundFetch, so a missing outbound scope is downgraded to a direct request instead of failing closed. Use scopedOutboundFetch for this connector path (or pass it explicitly) so scope loss cannot bypass organization egress.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/connectors/google-drive/google-drive-errors.ts, line 173:
<comment>Google Drive knowledge requests now use the fallback `outboundFetch`, so a missing outbound scope is downgraded to a direct request instead of failing closed. Use `scopedOutboundFetch` for this connector path (or pass it explicitly) so scope loss cannot bypass organization egress.</comment>
<file context>
@@ -169,7 +170,7 @@ export async function fetchGoogleDriveWithRetry(
return retryWithExponentialBackoff(
async () => {
- const response = await fetch(url, options)
+ const response = await outboundFetch(url, options)
if (response.ok) return response
</file context>
|
|
||
| const url = `${DRIVE_API_BASE}/changes/startPageToken?${params.toString()}` | ||
| const response = await fetch(url, { | ||
| const response = await outboundFetch(url, { |
There was a problem hiding this comment.
P2: A gateway or routing outage now throws from outboundFetch, and this poller's generic catch counts it with markWebhookFailed. After enough consecutive polls the Drive trigger is auto-disabled, even though the source is fine. Consider classifying OutboundRoutingError in this catch, or treating it like other source-fetch failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/lib/webhooks/polling/google-drive.ts, line 214:
<comment>A gateway or routing outage now throws from `outboundFetch`, and this poller's generic catch counts it with `markWebhookFailed`. After enough consecutive polls the Drive trigger is auto-disabled, even though the source is fine. Consider classifying `OutboundRoutingError` in this catch, or treating it like other source-fetch failures.</comment>
<file context>
@@ -210,7 +211,7 @@ async function getStartPageToken(
const url = `${DRIVE_API_BASE}/changes/startPageToken?${params.toString()}`
- const response = await fetch(url, {
+ const response = await outboundFetch(url, {
headers: { Authorization: `Bearer ${accessToken}` },
})
</file context>
| logger.info('Successfully refreshed access token', logContext) | ||
| return { kind: 'token', accessToken: result.accessToken } | ||
| } catch (error) { | ||
| if (error instanceof OutboundRoutingError) return { kind: 'egress_unavailable', error } |
There was a problem hiding this comment.
P2: A follower that waits on another process's refresh gets a generic 401 during an egress outage, while the leader gets the 503. The leader's egress_unavailable outcome is not visible to followers, so the same outage maps to two different responses. Have the follower path surface the egress outage, or map a null follower result to the egress error when the lease holder failed on routing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/lib/oauth/credential-service.ts, line 1856:
<comment>A follower that waits on another process's refresh gets a generic 401 during an egress outage, while the leader gets the 503. The leader's `egress_unavailable` outcome is not visible to followers, so the same outage maps to two different responses. Have the follower path surface the egress outage, or map a null follower result to the egress error when the lease holder failed on routing.</comment>
<file context>
@@ -1850,6 +1853,7 @@ async function performCoalescedRefresh({
logger.info('Successfully refreshed access token', logContext)
return { kind: 'token', accessToken: result.accessToken }
} catch (error) {
+ if (error instanceof OutboundRoutingError) return { kind: 'egress_unavailable', error }
logger.error('Refresh failed inside leader path', {
...logContext,
</file context>
| // A keyless refusal already names its cause and code; wrapping it would lose both. | ||
| if (error instanceof KeylessCredentialRefusedError) return error | ||
| // A refused or unavailable egress route says nothing about the credential. | ||
| if (error instanceof OutboundRoutingError) return error |
There was a problem hiding this comment.
P2: The new OutboundRoutingError passthrough here never reaches the caller. buildAwsServiceAccountSecret in service-account-secret.ts wraps every error except AwsCredentialError as a ServiceAccountSecretError, and credential create maps that to a 400. The pin probe in assertRolePinsSubject has the same gap: it wraps the error into an AwsCredentialError. Let egress errors pass through both catches so they reach the 503 mapping.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/lib/credentials/aws-service-account.server.ts, line 285:
<comment>The new `OutboundRoutingError` passthrough here never reaches the caller. `buildAwsServiceAccountSecret` in `service-account-secret.ts` wraps every error except `AwsCredentialError` as a `ServiceAccountSecretError`, and credential create maps that to a 400. The pin probe in `assertRolePinsSubject` has the same gap: it wraps the error into an `AwsCredentialError`. Let egress errors pass through both catches so they reach the 503 mapping.</comment>
<file context>
@@ -280,6 +281,8 @@ function bindSubject(
// A keyless refusal already names its cause and code; wrapping it would lose both.
if (error instanceof KeylessCredentialRefusedError) return error
+ // A refused or unavailable egress route says nothing about the credential.
+ if (error instanceof OutboundRoutingError) return error
const detail = failureClause(error)
if (config.authType === 'oidc_role') {
</file context>
| }) | ||
| } catch { | ||
| } catch (error) { | ||
| if (error instanceof OutboundRoutingError) { |
There was a problem hiding this comment.
P2: The new 503 mapping covers only the code exchange. managed.verifyIdentity also performs outboundFetch calls, but its catch still converts gateway outages to 502; preserve OutboundRoutingError as a 503 there too.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/lib/credential-groups/standard-oauth-provider.ts, line 319:
<comment>The new 503 mapping covers only the code exchange. `managed.verifyIdentity` also performs `outboundFetch` calls, but its catch still converts gateway outages to 502; preserve `OutboundRoutingError` as a 503 there too.</comment>
<file context>
@@ -306,14 +308,20 @@ export function createStandardOAuthCredentialGroupProviderAdapter(
})
- } catch {
+ } catch (error) {
+ if (error instanceof OutboundRoutingError) {
+ throw new CredentialGroupOAuthError(
+ `${service.name} authorization is temporarily unavailable.`,
</file context>
| } | ||
|
|
||
| const response = await fetch(apiUrl, { | ||
| const response = await postWandRequest(workspaceId, { |
There was a problem hiding this comment.
P2: Egress failures from postWandRequest are returned as HTTP 500, not 503. Map OutboundRoutingError gateway/configuration failures to 503 in both the streaming and non-streaming error paths so gateway outages remain retryable and are not treated as ordinary generation failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/app/api/wand/route.ts, line 374:
<comment>Egress failures from `postWandRequest` are returned as HTTP 500, not 503. Map `OutboundRoutingError` gateway/configuration failures to 503 in both the streaming and non-streaming error paths so gateway outages remain retryable and are not treated as ordinary generation failures.</comment>
<file context>
@@ -342,26 +356,22 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
- }
-
- const response = await fetch(apiUrl, {
+ const response = await postWandRequest(workspaceId, {
method: 'POST',
headers,
</file context>
| ): Promise<ProviderResponse | StreamingExecution> { | ||
| const { logger } = config | ||
| const fetchImpl = config.fetch ?? fetch | ||
| const fetchImpl = config.fetch ?? outboundFetch |
There was a problem hiding this comment.
P2: Pass global fetch for operator-configured Azure endpoints; this default routes them through the active organization's gateway when the Azure caller leaves fetch unset.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/providers/openai/core.ts, line 160:
<comment>Pass global `fetch` for operator-configured Azure endpoints; this default routes them through the active organization's gateway when the Azure caller leaves `fetch` unset.</comment>
<file context>
@@ -156,7 +157,7 @@ export async function executeResponsesProviderRequest(
): Promise<ProviderResponse | StreamingExecution> {
const { logger } = config
- const fetchImpl = config.fetch ?? fetch
+ const fetchImpl = config.fetch ?? outboundFetch
logger.info(`Preparing ${config.providerLabel} request`, {
</file context>
| } | ||
|
|
||
| const response = await fetch( | ||
| const response = await outboundFetch( |
There was a problem hiding this comment.
P3: A gateway outage raised by outboundFetch here propagates past sendTelegramDocument's handlers and reaches executeTelegramTool, which returns 500 for it. The PR maps egress outages to 503, so map OutboundRoutingError (GATEWAY_UNAVAILABLE) to 503 in execute-tool.ts, or wrap this call.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/lib/internal/telegram/operations.ts, line 104:
<comment>A gateway outage raised by `outboundFetch` here propagates past `sendTelegramDocument`'s handlers and reaches `executeTelegramTool`, which returns 500 for it. The PR maps egress outages to 503, so map `OutboundRoutingError` (GATEWAY_UNAVAILABLE) to 503 in `execute-tool.ts`, or wrap this call.</comment>
<file context>
@@ -100,7 +101,7 @@ export async function sendTelegramDocument(
}
- const response = await fetch(
+ const response = await outboundFetch(
`https://api.telegram.org/bot${encodeURIComponent(input.botToken)}/sendDocument`,
{ method: 'POST', body: form, signal: context.signal }
</file context>
…etch module and type the new route tests
…ken route removal resolveCredentialAccessToken now takes the execution's userId and builds the internal-JWT auth itself instead of an authenticate callback; resolveCredentialToken is private. Removes the session/API-key saved-secret refusal no caller can reach, the auditRequest plumbing the executor never sets (recordCredentialAccess keeps it for selectors), and the unparsed oauthTokenResponseSchema in favor of a plain CredentialTokenPayload type.
…n unavailable orchestration error Add an 'unavailable' OrchestrationErrorCode (503, v2 SERVICE_UNAVAILABLE) and let asOrchestrationError classify an OutboundRoutingError anywhere in the cause chain with a fixed message, so no routing code reaches callers. The standard OAuth credential-group adapter now rethrows routing failures from code exchange and identity verification instead of wrapping them locally.
…le code in token resolution, managed OAuth and Slack
…orized use cases The wand route and the baseten, fireworks, together and ollama-cloud model routes authorized inline and set the outbound scope per call through fetchForWorkspace. Each now authenticates, parses and presents only; the use case loads the workspace, authorizes, picks the BYOK or Sim key and calls the vendor with scopedOutboundFetch inside the workspace's outbound scope. Wand splits into a workflow operation (write) and a workspace operation (read) so each declares its real minimum role. A model listing for a caller outside the workspace still falls back to Sim's key, direct. fetchForWorkspace has no callers left and is removed.
…ganization-egress
Summary
secureFetch*and generic tool HTTP were routed; rawfetchand the AWS SDK always used shared IPs, so token mints, OAuth refreshes, tenant API calls and many tool clients bypassed the gatewayoutboundFetch(lib/core/network/outbound-fetch.server.ts), a drop-in forfetch: gateway org → gateway; every other route → the globalfetch, unchanged. Outside any scope it goes direct with a warning instead of failing closed, so a path that never set a scope can't become an outage.scopedOutboundFetchkeeps knowledge connectors fail-closed as beforeorganizationAwsRequestHandlerfor AWS SDK clients signing with customer credentials (STS, AWS tools, BYOK Bedrock): gateway orgs tunnel per org (no socket shared across orgs); everyone else keeps the exact handler they had, including Bedrock's HTTP/2fetchProvider, Databricks, NetSuite, SailPoint, CrowdStrike, Entra, Google WIF, Atlassian, Snowflake PAT), OAuth refresh, OAuth connect (custom exchanges, plus a routed default code exchange built from better-auth's own request builder),lib/internaltool clients, tools, webhook subscriptions and polling, LLM/embedding providers (incl. Gemini/Vertex viahttpOptions.fetch), data drains, selectors/api/auth/oauth/token(GET/POST) and its contracts and route-only helpers: nothing calls it since in-process token resolution (improvement(oauth): resolve credential tokens in-process instead of hopping the token route #7340), and no sibling repo references itcheck:organization-egressAST audit: a globalfetchreference in code that acts for a workspace fails unless annotated// egress-direct: <reason>; the rule is documented in CLAUDE.mdType of Change
Testing
outboundFetchand the AWS handler go through the scoped org's gateway, each org gets its own tunnel, and pooled sockets are never shared across orgs (fail when replaced with plainfetch/awsRequestHandler/ a shared pool)check:organization-egress(+ its tests),check:egress-boundary,check:api-validation:strict,check:test-patterns,check:boundaries,check:client-boundary,check:explicit-any,check:source-conventions,check:file-names,check:comment-hygiene, lint and type-check pass locally; full suite andcheck:auditsin CIChecklist
test-auditauthoring gate)