@@ -5,15 +5,13 @@ import { getErrorMessage, 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 ,
@@ -773,11 +772,15 @@ async function driveOneChildChain(
773772 options . onAbortObserved ?.( reason )
774773 } ,
775774 }
775+ // Same per-leg BYOK rule as the main loop: this child-chain resume can also land on
776+ // a dead run and become a hosted-key continuation without it.
777+ const byokApiKey = await resolveEnterpriseByokKey ( workspaceId )
776778 await runResumeLegWithRetry (
777779 `${ baseURL } /api/tools/resume` ,
778780 {
779781 streamId : context . messageId ,
780782 results,
783+ ...( byokApiKey ? { byokApiKey } : { } ) ,
781784 } ,
782785 leg ,
783786 execContext ,
@@ -919,15 +922,15 @@ async function runCheckpointLoop(
919922 payload = { ...payload , workspaceId : lifecycleWorkspaceId }
920923 }
921924
922- // Enterprise BYOK eligibility hint: set once on the initial mothership request
923- // so Go only attempts a BYOK lookup for entitled workspaces. This is only a
924- // gate — Go re-confirms entitlement authoritatively before using any key.
925- payload = await withEnterpriseByokKey ( payload , route , lifecycleWorkspaceId )
926-
927925 for ( ; ; ) {
928926 context . streamComplete = false
929927 const isResume = route === '/api/tools/resume'
930928
929+ // Enterprise BYOK rides EVERY leg, resume included: a resume that lands on a dead
930+ // run becomes a continuation with no closure holding the key. Re-resolved per leg so
931+ // revocation is immediate (key rows are read fresh; entitlement is cached).
932+ payload = await withEnterpriseByokKey ( payload , route , lifecycleWorkspaceId )
933+
931934 if ( isResume && isAborted ( options , context ) ) {
932935 cancelPendingTools ( context )
933936 context . awaitingAsyncContinuation = undefined
@@ -1437,30 +1440,26 @@ async function ensureHeadlessRunIdentity(input: {
14371440// Helpers
14381441
14391442/**
1440- * Resolves the enterprise BYOK key sim-side and attaches it as `byokApiKey`
1441- * (contract field, S27): the worker builds a per-run provider instance from it and
1442- * retains nothing. Eligibility (enterprise plan) gates resolution server-side, so a
1443- * client can never assert its own eligibility; key rows are read fresh so revocation
1444- * is immediate. Failures default to hosted. Mothership-only — other routes untouched.
1443+ * Routes whose payloads carry `byokApiKey` (see resolveEnterpriseByokKey): every
1444+ * model-reaching worker call, INCLUDING tool-resume — a resume that lands on a dead run
1445+ * becomes a continuation leg with no closure holding the key, so omitting it there
1446+ * silently finishes an enterprise chat on the hosted key.
14451447 */
1448+ const BYOK_ROUTES = [
1449+ '/api/mothership' ,
1450+ '/api/mothership/execute' ,
1451+ '/api/copilot' ,
1452+ '/api/tools/resume' ,
1453+ ]
1454+
14461455async function withEnterpriseByokKey (
14471456 payload : Record < string , unknown > ,
14481457 route : string ,
14491458 workspaceId ?: string
14501459) : Promise < Record < string , unknown > > {
1451- if ( ! workspaceId || ! route . startsWith ( '/api/mothership' ) ) return payload
1452- try {
1453- if ( ! ( await isWorkspaceOnEnterprisePlan ( workspaceId ) ) ) return payload
1454- const byok = await getBYOKKey ( workspaceId , 'anthropic' )
1455- if ( ! byok ) return payload
1456- return { ...payload , byokApiKey : byok . apiKey }
1457- } catch ( error ) {
1458- logger . warn ( 'Failed to resolve BYOK key; defaulting to hosted' , {
1459- workspaceId,
1460- error : toError ( error ) . message ,
1461- } )
1462- return payload
1463- }
1460+ if ( ! BYOK_ROUTES . includes ( route ) ) return payload
1461+ const byokApiKey = await resolveEnterpriseByokKey ( workspaceId )
1462+ return byokApiKey ? { ...payload , byokApiKey } : payload
14641463}
14651464
14661465function isAborted ( options : CopilotLifecycleOptions , context : StreamingContext ) : boolean {
0 commit comments