@@ -5,15 +5,13 @@ import { toError } from '@sim/utils/errors'
55import { interruptibleSleep , sleep } from '@sim/utils/helpers'
66import { generateId } from '@sim/utils/id'
77import { omit } from '@sim/utils/object'
8- import { getBYOKKey } from '@/lib/api-key/byok'
98import {
109 type AttributedBillingRequestEnvelope ,
1110 assertBillingAttributionSnapshot ,
1211 type BillingAttributionSnapshot ,
1312 checkAttributedUsageLimits ,
1413 createAttributedBillingRequestEnvelope ,
1514} from '@/lib/billing/core/billing-attribution'
16- import { isWorkspaceOnEnterprisePlan } from '@/lib/billing/core/subscription'
1715import { env } from '@/lib/core/config/env'
1816import { isCopilotToolPermissionsEnabled , isHosted } from '@/lib/core/config/env-flags'
1917import type { AsyncCompletionSignal } from '@/lib/mothership/async-runs/lifecycle'
@@ -33,6 +31,7 @@ import { CopilotDegradedReason } from '@/lib/mothership/generated/trace-attribut
3331import { getAutoAllowedTools } from '@/lib/mothership/persistence/tool-permission/auto-allow'
3432import { createStreamingContext } from '@/lib/mothership/request/context/request-context'
3533import { buildToolCallSummaries } from '@/lib/mothership/request/context/result'
34+ import { resolveEnterpriseByokKey } from '@/lib/mothership/request/enterprise-byok'
3635import {
3736 BillingLimitError ,
3837 CopilotBackendError ,
@@ -733,11 +732,15 @@ async function driveOneChildChain(
733732 options . onAbortObserved ?.( reason )
734733 } ,
735734 }
735+ // Same per-leg BYOK rule as the main loop: this child-chain resume can also land on
736+ // a dead run and become a hosted-key continuation without it.
737+ const byokApiKey = await resolveEnterpriseByokKey ( workspaceId )
736738 await runResumeLegWithRetry (
737739 `${ baseURL } /api/tools/resume` ,
738740 {
739741 streamId : context . messageId ,
740742 results,
743+ ...( byokApiKey ? { byokApiKey } : { } ) ,
741744 } ,
742745 leg ,
743746 execContext ,
@@ -879,15 +882,15 @@ async function runCheckpointLoop(
879882 payload = { ...payload , workspaceId : lifecycleWorkspaceId }
880883 }
881884
882- // Enterprise BYOK eligibility hint: set once on the initial mothership request
883- // so Go only attempts a BYOK lookup for entitled workspaces. This is only a
884- // gate — Go re-confirms entitlement authoritatively before using any key.
885- payload = await withEnterpriseByokKey ( payload , route , lifecycleWorkspaceId )
886-
887885 for ( ; ; ) {
888886 context . streamComplete = false
889887 const isResume = route === '/api/tools/resume'
890888
889+ // Enterprise BYOK rides EVERY leg, resume included: a resume that lands on a dead
890+ // run becomes a continuation with no closure holding the key. Re-resolved per leg so
891+ // revocation is immediate (key rows are read fresh; entitlement is cached).
892+ payload = await withEnterpriseByokKey ( payload , route , lifecycleWorkspaceId )
893+
891894 if ( isResume && isAborted ( options , context ) ) {
892895 cancelPendingTools ( context )
893896 context . awaitingAsyncContinuation = undefined
@@ -1397,30 +1400,26 @@ async function ensureHeadlessRunIdentity(input: {
13971400// Helpers
13981401
13991402/**
1400- * Resolves the enterprise BYOK key sim-side and attaches it as `byokApiKey`
1401- * (contract field, S27): the worker builds a per-run provider instance from it and
1402- * retains nothing. Eligibility (enterprise plan) gates resolution server-side, so a
1403- * client can never assert its own eligibility; key rows are read fresh so revocation
1404- * is immediate. Failures default to hosted. Mothership-only — other routes untouched.
1403+ * Routes whose payloads carry `byokApiKey` (see resolveEnterpriseByokKey): every
1404+ * model-reaching worker call, INCLUDING tool-resume — a resume that lands on a dead run
1405+ * becomes a continuation leg with no closure holding the key, so omitting it there
1406+ * silently finishes an enterprise chat on the hosted key.
14051407 */
1408+ const BYOK_ROUTES = [
1409+ '/api/mothership' ,
1410+ '/api/mothership/execute' ,
1411+ '/api/copilot' ,
1412+ '/api/tools/resume' ,
1413+ ]
1414+
14061415async function withEnterpriseByokKey (
14071416 payload : Record < string , unknown > ,
14081417 route : string ,
14091418 workspaceId ?: string
14101419) : Promise < Record < string , unknown > > {
1411- if ( ! workspaceId || ! route . startsWith ( '/api/mothership' ) ) return payload
1412- try {
1413- if ( ! ( await isWorkspaceOnEnterprisePlan ( workspaceId ) ) ) return payload
1414- const byok = await getBYOKKey ( workspaceId , 'anthropic' )
1415- if ( ! byok ) return payload
1416- return { ...payload , byokApiKey : byok . apiKey }
1417- } catch ( error ) {
1418- logger . warn ( 'Failed to resolve BYOK key; defaulting to hosted' , {
1419- workspaceId,
1420- error : toError ( error ) . message ,
1421- } )
1422- return payload
1423- }
1420+ if ( ! BYOK_ROUTES . includes ( route ) ) return payload
1421+ const byokApiKey = await resolveEnterpriseByokKey ( workspaceId )
1422+ return byokApiKey ? { ...payload , byokApiKey } : payload
14241423}
14251424
14261425function isAborted ( options : CopilotLifecycleOptions , context : StreamingContext ) : boolean {
0 commit comments