From 6f8a969990ef2440ec757203182bbfa2483656b5 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Sun, 23 Aug 2026 15:33:33 -0400 Subject: [PATCH 1/9] fix(messaging): retain stopped channel provider bindings Signed-off-by: Julie Yaunches --- .../initial-policy-real-policy.test.ts | 10 +++ src/lib/onboard/sandbox-create-intent.ts | 32 ++------- .../sandbox-create-plan-materialization.ts | 49 ++++--------- src/lib/onboard/sandbox-create-plan.test.ts | 71 ++++++++++++++++++- 4 files changed, 98 insertions(+), 64 deletions(-) diff --git a/src/lib/onboard/initial-policy-real-policy.test.ts b/src/lib/onboard/initial-policy-real-policy.test.ts index 1364785211e..63ff814f014 100644 --- a/src/lib/onboard/initial-policy-real-policy.test.ts +++ b/src/lib/onboard/initial-policy-real-policy.test.ts @@ -291,6 +291,16 @@ describe("initial sandbox policy real preset merge", () => { expect(discordBinaries).toContain("/opt/hermes/.venv/bin/python"); expect(discordBinaries).not.toContain("/usr/bin/node"); + const boundProviders = + policy.network_policies?.discord?.endpoints + ?.map((endpoint) => endpoint.credential_binding?.provider) + .filter(Boolean) ?? []; + expect(boundProviders).toEqual([ + "hermes-channel-discord-bridge", + "hermes-channel-discord-bridge", + "hermes-channel-discord-bridge", + ]); + const discordRules = policy.network_policies?.discord?.endpoints ?.find((endpoint) => endpoint.host === "discord.com") diff --git a/src/lib/onboard/sandbox-create-intent.ts b/src/lib/onboard/sandbox-create-intent.ts index d7372fd70ba..757dd4ec0f4 100644 --- a/src/lib/onboard/sandbox-create-intent.ts +++ b/src/lib/onboard/sandbox-create-intent.ts @@ -27,27 +27,6 @@ function filterMessagingProviderRequestsByEnabledChannel( return requests.filter(({ channel }) => !channel || !disabledChannelNames.has(channel)); } -function resolveTokenProviderChannelMap( - requests: readonly SandboxCreateMessagingProviderRequest[], -): Map { - const providerChannels = new Map(); - for (const { channel, name } of requests) { - if (channel) providerChannels.set(name, channel); - } - return providerChannels; -} - -function filterMessagingProvidersByEnabledChannel( - providerNames: string[], - providerChannels: ReadonlyMap, - disabledChannelNames: ReadonlySet, -): string[] { - return providerNames.filter((providerName) => { - const channel = providerChannels.get(providerName); - return !channel || !disabledChannelNames.has(channel); - }); -} - function resolveActiveMessagingChannels({ channels, disabledChannelNames, @@ -157,7 +136,6 @@ export function resolveSandboxCreateIntent({ messagingProviderRequests, disabledChannelNames, ); - const providerChannels = resolveTokenProviderChannelMap(messagingProviderRequests); const activeMessagingChannels = resolveActiveMessagingChannels({ channels, disabledChannelNames, @@ -166,11 +144,11 @@ export function resolveSandboxCreateIntent({ primaryMessagingCredentialEnvKeys, reusableMessagingChannels, }); - const enabledReusableMessagingProviders = filterMessagingProvidersByEnabledChannel( - [...new Set(reusableMessagingProviders)], - providerChannels, - disabledChannelNames, - ); + // A credential-bound policy can outlive an active channel while the channel is + // stopped, Shields is lowered, or a rebuild replays preserved policy state. + // Keep every exact reusable provider attached to the replacement sandbox; the + // disabled plan still suppresses channel startup, render, and runtime effects. + const enabledReusableMessagingProviders = [...new Set(reusableMessagingProviders)]; const normalizedInferenceProvider = inferenceProvider?.trim() || null; diff --git a/src/lib/onboard/sandbox-create-plan-materialization.ts b/src/lib/onboard/sandbox-create-plan-materialization.ts index d59133d8416..709ff7320e6 100644 --- a/src/lib/onboard/sandbox-create-plan-materialization.ts +++ b/src/lib/onboard/sandbox-create-plan-materialization.ts @@ -192,27 +192,6 @@ export function validateSandboxCreateIntentBindings( }); } -function resolveProviderChannelMap( - requests: readonly SandboxCreateMessagingProviderRequest[], -): Map { - const providerChannels = new Map(); - for (const { channel, name } of requests) { - if (channel) providerChannels.set(name, channel); - } - return providerChannels; -} - -function filterDisabledMessagingProviders( - providerNames: string[], - providerChannels: ReadonlyMap, - disabledChannelNames: ReadonlySet, -): string[] { - return providerNames.filter((providerName) => { - const channel = providerChannels.get(providerName); - return !channel || !disabledChannelNames.has(channel); - }); -} - /** Materialize policy, route metadata, resources, and providers from a secretless intent. */ export function materializeSandboxCreatePlan({ intent, @@ -265,20 +244,20 @@ export function materializeSandboxCreatePlan({ ]; runProviderPreDeleteCleanup(); - const providerChannels = resolveProviderChannelMap(intent.messagingProviderRequests); - const messagingProviders = filterDisabledMessagingProviders( - [ - ...new Set([ - ...upsertMessagingProviders(enabledMessagingTokenDefs, { - replaceExisting: true, - allowedSandboxes: [intent.sandboxName], - }), - ...intent.reusableMessagingProviders, - ]), - ], - providerChannels, - new Set(intent.disabledChannelNames), - ); + // Reusable providers hold gateway-side credential authority. Keep their exact + // attachment on the replacement even when the channel runtime is disabled: + // the initial policy can still carry a credential_binding during rebuild or + // Shields transitions, while enabledMessagingTokenDefs continues to suppress + // new provider creation for disabled channels. + const messagingProviders = [ + ...new Set([ + ...upsertMessagingProviders(enabledMessagingTokenDefs, { + replaceExisting: true, + allowedSandboxes: [intent.sandboxName], + }), + ...intent.reusableMessagingProviders, + ]), + ]; const createProviders = new Set(); if (intent.inferenceProvider) createProviders.add(intent.inferenceProvider); for (const provider of messagingProviders) createProviders.add(provider); diff --git a/src/lib/onboard/sandbox-create-plan.test.ts b/src/lib/onboard/sandbox-create-plan.test.ts index fe736607334..a00616316d3 100644 --- a/src/lib/onboard/sandbox-create-plan.test.ts +++ b/src/lib/onboard/sandbox-create-plan.test.ts @@ -23,6 +23,12 @@ const selectedSandboxGpuConfig: SandboxGpuCreateConfig = { sandboxGpuDevice: "nvidia.com/gpu=0", }; +const disabledSandboxGpuConfig: SandboxGpuCreateConfig = { + sandboxGpuEnabled: false, + sandboxGpuDevice: null, + hostGpuDetected: false, +}; + afterEach(() => { vi.unstubAllEnvs(); }); @@ -150,7 +156,7 @@ describe("resolveSandboxCreateIntent", () => { expect(JSON.stringify(requests)).not.toContain("telegram-super-secret"); }); - it("resolves deterministic serializable intent without execution artifacts", () => { + it("resolves serializable intent and keeps stopped-channel providers attached (#9773)", () => { const input = { basePolicyPath: "/repo/policy.yaml", sandboxName: "sandbox", @@ -206,7 +212,10 @@ describe("resolveSandboxCreateIntent", () => { "sandbox-telegram-bridge", "sandbox-slack-bridge", ]); - expect(first.reusableMessagingProviders).toEqual(["sandbox-existing-discord"]); + expect(first.reusableMessagingProviders).toEqual([ + "sandbox-existing-discord", + "sandbox-slack-bridge", + ]); expect(first.extraProviders).toEqual(["custom-provider"]); expect(first.staleExtraProviders).toEqual(["stale-provider"]); expect(first.resourceCreateArgs).toEqual(["--cpu", "4", "--memory", "16Gi"]); @@ -237,6 +246,64 @@ describe("resolveSandboxCreateIntent", () => { expect(JSON.stringify(first)).not.toContain("/tmp/"); }); + it("attaches an exact reusable provider while its channel runtime is stopped (#9773)", () => { + const intent = resolveSandboxCreateIntent({ + basePolicyPath: "/repo/hermes-policy.yaml", + sandboxName: "sandbox", + channels, + enabledChannels: ["discord"], + disabledChannelNames: new Set(["discord"]), + messagingProviderRequests: [ + { + name: "sandbox-discord-bridge", + envKey: "DISCORD_BOT_TOKEN", + providerType: "discord-hermes-static-v1", + credentialConfigured: false, + channel: "discord", + }, + ], + primaryMessagingCredentialEnvKeys: ["DISCORD_BOT_TOKEN"], + reusableMessagingChannels: [], + reusableMessagingProviders: ["sandbox-discord-bridge"], + extraProviders: [], + hermesToolGateways: [], + sandboxGpuConfig: disabledSandboxGpuConfig, + gpuCreateArgs: [], + gpuRoutePlan: "none", + sandboxGpuLogMessage: null, + agentName: "hermes", + policyTier: "balanced", + }); + const upsertMessagingProviders = vi.fn(() => []); + + const plan = materializeSandboxCreatePlan({ + intent, + fromRef: "/tmp/Dockerfile", + messagingTokenDefs: [ + { + name: "sandbox-discord-bridge", + envKey: "DISCORD_BOT_TOKEN", + token: null, + providerType: "discord-hermes-static-v1", + }, + ], + runProviderPreDeleteCleanup: vi.fn(), + upsertMessagingProviders, + getHermesToolGatewayProviderName: vi.fn(), + prepareInitialSandboxCreatePolicy: vi.fn(() => ({ + policyPath: "/tmp/policy.yaml", + appliedPresets: [], + })), + }); + + expect(upsertMessagingProviders).toHaveBeenCalledWith([], { + replaceExisting: true, + allowedSandboxes: ["sandbox"], + }); + expect(plan.messagingProviders).toEqual(["sandbox-discord-bridge"]); + expect(plan.createArgs).toContain("sandbox-discord-bridge"); + }); + it("keeps the real gateway provider while excluding direct host-local inference policy", () => { const intent = resolveSandboxCreateIntent({ basePolicyPath: "/repo/policy.yaml", From 6897e5d73713755638c1797cbeeb1d715dcba054 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Sun, 23 Aug 2026 20:35:59 -0400 Subject: [PATCH 2/9] fix(messaging): discover stopped channel providers Signed-off-by: Julie Yaunches --- src/lib/onboard/messaging-prep.test.ts | 10 +++++----- src/lib/onboard/messaging-prep.ts | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/lib/onboard/messaging-prep.test.ts b/src/lib/onboard/messaging-prep.test.ts index 015fda9710d..13c28f3cd30 100644 --- a/src/lib/onboard/messaging-prep.test.ts +++ b/src/lib/onboard/messaging-prep.test.ts @@ -228,9 +228,7 @@ describe("prepareCreateSandboxMessaging", () => { expect(result.missingBridgeChannels).toEqual(["googlechat"]); }); - it("does not reuse the bridge provider of a disabled channel", () => { - // The matcher would accept this provider, so only the disabled guard can - // keep it out — and a disabled channel is not a missing one either. + it("retains an exact bridge provider while its channel runtime is disabled (#9773)", () => { const providerMatchesGatewayCredential = vi.fn(() => true); const result = prepareCreateSandboxMessaging( @@ -241,9 +239,11 @@ describe("prepareCreateSandboxMessaging", () => { }), ); - expect(result.reusableMessagingProviders).toEqual([]); + expect(result.reusableMessagingProviders).toEqual(["demo-googlechat-bridge"]); + expect(result.reusableMessagingChannels).toEqual(["googlechat"]); + expect(result.messagingTokenDefs.map(({ name }) => name)).not.toContain("demo-googlechat-bridge"); expect(result.missingBridgeChannels).toEqual([]); - expect(providerMatchesGatewayCredential).not.toHaveBeenCalled(); + expect(providerMatchesGatewayCredential).toHaveBeenCalled(); }); it("reports missing Brave API keys before registering extra placeholder providers", () => { diff --git a/src/lib/onboard/messaging-prep.ts b/src/lib/onboard/messaging-prep.ts index ce6284cb32c..b53fe8675a0 100644 --- a/src/lib/onboard/messaging-prep.ts +++ b/src/lib/onboard/messaging-prep.ts @@ -82,7 +82,7 @@ export function prepareCreateSandboxMessaging( ); const messagingProviderProfiles = messagingBridgeProfilesForAgent(input.agentName); - const messagingTokenDefs: MessagingTokenDef[] = listMessagingCredentialMetadata() + const messagingCredentialDefs: MessagingTokenDef[] = listMessagingCredentialMetadata() .map((credential) => ({ name: credential.providerNameTemplate.replaceAll("{sandboxName}", input.sandboxName), envKey: credential.providerEnvKey, @@ -94,8 +94,10 @@ export function prepareCreateSandboxMessaging( messagingProviderProfiles, ) ?? MESSAGING_CREDENTIAL_PROVIDER_TYPE, })) - .filter(({ envKey }) => !enabledEnvKeys || enabledEnvKeys.has(envKey)) - .filter(({ envKey }) => !disabledEnvKeys.has(envKey)); + .filter(({ envKey }) => !enabledEnvKeys || enabledEnvKeys.has(envKey)); + const messagingTokenDefs = messagingCredentialDefs.filter( + ({ envKey }) => !disabledEnvKeys.has(envKey), + ); const webSearchEnabled = braveProviderProfile.shouldEnableWebSearch(input.webSearchConfig); const webSearchProvider = webSearch.webSearchProviderForConfig(input.webSearchConfig); @@ -178,7 +180,7 @@ export function prepareCreateSandboxMessaging( const reusableMessagingChannels: string[] = []; if (input.enabledChannels != null) { - for (const { name, envKey, token, providerType } of messagingTokenDefs) { + for (const { name, envKey, token, providerType } of messagingCredentialDefs) { if (token) continue; const channel = input.getMessagingChannelForEnvKey(envKey); if (!channel || !input.enabledChannels.includes(channel)) continue; @@ -205,7 +207,6 @@ export function prepareCreateSandboxMessaging( for (const profile of bridgeProfiles) { const channel = profile.channelId; if (!input.enabledChannels.includes(channel)) continue; - if (disabledChannelNames.has(channel)) continue; for (const name of bridgeProviderNamesForChannel(input.sandboxName, channel, [profile])) { if (messagingTokenDefs.some((def) => def.name === name && def.token)) continue; if (reusableMessagingProviders.includes(name)) continue; From bb7dfcf8e7af4b02ab536b3ecee024ab81625bc8 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Sun, 23 Aug 2026 20:16:07 -0400 Subject: [PATCH 3/9] fix(images): pass managed target architecture Signed-off-by: Julie Yaunches --- .github/workflows/managed-images.yaml | 4 ++++ test/managed-image-publication-workflow.test.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 9b78f4faf64..8caf027abc1 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -1890,9 +1890,12 @@ jobs: DOCKERFILE: ${{ matrix.dockerfile }} run: | set -euo pipefail + target_arch='${{ matrix.arch }}' + case "$target_arch" in amd64|arm64) ;; *) exit 1 ;; esac build_args=( -f "$DOCKERFILE" --build-arg "BASE_IMAGE=${BASE_IMAGE}" + --build-arg "TARGETARCH=${target_arch}" --build-arg "NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1" --build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root" ) @@ -1922,6 +1925,7 @@ jobs: ${{ matrix.agent == 'langchain-deepagents-code' && format('com.nvidia.nemoclaw.base-resolution={0}', steps.base.outputs.resolution_label) || '' }} build-args: | BASE_IMAGE=${{ steps.base.outputs.ref }} + TARGETARCH=${{ matrix.arch }} NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1 NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.artifact_platform }} diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index aa81a87231b..00a2bc4c4dc 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -956,6 +956,7 @@ fi expect(releaseIdentity.id).toBe("release"); expect(releaseIdentity.run).toContain("git describe --tags --match 'v*' \"$GITHUB_SHA\""); expect(releaseIdentity.run).toContain("managed image release identity does not match"); + expect(guard.run).toContain('--build-arg "TARGETARCH=${target_arch}"'); expect(guard.run).toContain('scripts/check-production-build-args.sh "${build_args[@]}"'); expect(build.uses).toBe("docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a"); expect(build.with).toMatchObject({ @@ -963,7 +964,7 @@ fi file: "${{ matrix.dockerfile }}", platforms: "${{ matrix.platform }}", "build-args": - "BASE_IMAGE=${{ steps.base.outputs.ref }}\nNEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1\nNEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root\n", + "BASE_IMAGE=${{ steps.base.outputs.ref }}\nTARGETARCH=${{ matrix.arch }}\nNEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1\nNEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root\n", provenance: "mode=max", sbom: true, }); From 1c737383d61d86fa45a820c039978a81b8c7a45c Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sun, 23 Aug 2026 17:49:32 -0700 Subject: [PATCH 4/9] Revert "fix(images): pass managed target architecture" This reverts commit bb7dfcf8e7af4b02ab536b3ecee024ab81625bc8. --- .github/workflows/managed-images.yaml | 4 ---- test/managed-image-publication-workflow.test.ts | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 8caf027abc1..9b78f4faf64 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -1890,12 +1890,9 @@ jobs: DOCKERFILE: ${{ matrix.dockerfile }} run: | set -euo pipefail - target_arch='${{ matrix.arch }}' - case "$target_arch" in amd64|arm64) ;; *) exit 1 ;; esac build_args=( -f "$DOCKERFILE" --build-arg "BASE_IMAGE=${BASE_IMAGE}" - --build-arg "TARGETARCH=${target_arch}" --build-arg "NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1" --build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root" ) @@ -1925,7 +1922,6 @@ jobs: ${{ matrix.agent == 'langchain-deepagents-code' && format('com.nvidia.nemoclaw.base-resolution={0}', steps.base.outputs.resolution_label) || '' }} build-args: | BASE_IMAGE=${{ steps.base.outputs.ref }} - TARGETARCH=${{ matrix.arch }} NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1 NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.artifact_platform }} diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 00a2bc4c4dc..aa81a87231b 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -956,7 +956,6 @@ fi expect(releaseIdentity.id).toBe("release"); expect(releaseIdentity.run).toContain("git describe --tags --match 'v*' \"$GITHUB_SHA\""); expect(releaseIdentity.run).toContain("managed image release identity does not match"); - expect(guard.run).toContain('--build-arg "TARGETARCH=${target_arch}"'); expect(guard.run).toContain('scripts/check-production-build-args.sh "${build_args[@]}"'); expect(build.uses).toBe("docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a"); expect(build.with).toMatchObject({ @@ -964,7 +963,7 @@ fi file: "${{ matrix.dockerfile }}", platforms: "${{ matrix.platform }}", "build-args": - "BASE_IMAGE=${{ steps.base.outputs.ref }}\nTARGETARCH=${{ matrix.arch }}\nNEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1\nNEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root\n", + "BASE_IMAGE=${{ steps.base.outputs.ref }}\nNEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1\nNEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root\n", provenance: "mode=max", sbom: true, }); From 34af94e8ec5159fb0d3abc393ca99298d38fa928 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sun, 23 Aug 2026 18:17:32 -0700 Subject: [PATCH 5/9] fix(onboard): scope stopped provider retention Signed-off-by: Prekshi Vyas --- src/lib/onboard/messaging-prep.test.ts | 10 ++-- src/lib/onboard/messaging-prep.ts | 57 +++++++++++++------ src/lib/onboard/sandbox-create-intent.ts | 32 +++++++++-- .../sandbox-create-plan-materialization.ts | 49 +++++++++++----- src/lib/onboard/sandbox-create-plan.test.ts | 30 +++------- .../hermes-discord-credential-binding.test.ts | 20 ++++++- 6 files changed, 132 insertions(+), 66 deletions(-) diff --git a/src/lib/onboard/messaging-prep.test.ts b/src/lib/onboard/messaging-prep.test.ts index 13c28f3cd30..015fda9710d 100644 --- a/src/lib/onboard/messaging-prep.test.ts +++ b/src/lib/onboard/messaging-prep.test.ts @@ -228,7 +228,9 @@ describe("prepareCreateSandboxMessaging", () => { expect(result.missingBridgeChannels).toEqual(["googlechat"]); }); - it("retains an exact bridge provider while its channel runtime is disabled (#9773)", () => { + it("does not reuse the bridge provider of a disabled channel", () => { + // The matcher would accept this provider, so only the disabled guard can + // keep it out — and a disabled channel is not a missing one either. const providerMatchesGatewayCredential = vi.fn(() => true); const result = prepareCreateSandboxMessaging( @@ -239,11 +241,9 @@ describe("prepareCreateSandboxMessaging", () => { }), ); - expect(result.reusableMessagingProviders).toEqual(["demo-googlechat-bridge"]); - expect(result.reusableMessagingChannels).toEqual(["googlechat"]); - expect(result.messagingTokenDefs.map(({ name }) => name)).not.toContain("demo-googlechat-bridge"); + expect(result.reusableMessagingProviders).toEqual([]); expect(result.missingBridgeChannels).toEqual([]); - expect(providerMatchesGatewayCredential).toHaveBeenCalled(); + expect(providerMatchesGatewayCredential).not.toHaveBeenCalled(); }); it("reports missing Brave API keys before registering extra placeholder providers", () => { diff --git a/src/lib/onboard/messaging-prep.ts b/src/lib/onboard/messaging-prep.ts index b53fe8675a0..301999cd976 100644 --- a/src/lib/onboard/messaging-prep.ts +++ b/src/lib/onboard/messaging-prep.ts @@ -23,6 +23,11 @@ export interface MessagingTokenDef { providerType?: string; } +type MessagingCredentialDef = MessagingTokenDef & { + /** The stopped-channel policy still references this static provider. */ + retainWhileDisabled: boolean; +}; + export interface CreateSandboxMessagingPrepInput { sandboxName: string; agentName?: string | null; @@ -82,22 +87,25 @@ export function prepareCreateSandboxMessaging( ); const messagingProviderProfiles = messagingBridgeProfilesForAgent(input.agentName); - const messagingCredentialDefs: MessagingTokenDef[] = listMessagingCredentialMetadata() - .map((credential) => ({ - name: credential.providerNameTemplate.replaceAll("{sandboxName}", input.sandboxName), - envKey: credential.providerEnvKey, - token: input.getValidatedMessagingTokenByEnvKey(input.channels, credential.providerEnvKey), - providerType: - staticMessagingProviderTypeForChannel( - credential.channelId, - input.agentName, - messagingProviderProfiles, - ) ?? MESSAGING_CREDENTIAL_PROVIDER_TYPE, - })) + const messagingCredentialDefs: MessagingCredentialDef[] = listMessagingCredentialMetadata() + .map((credential) => { + const staticProviderType = staticMessagingProviderTypeForChannel( + credential.channelId, + input.agentName, + messagingProviderProfiles, + ); + return { + name: credential.providerNameTemplate.replaceAll("{sandboxName}", input.sandboxName), + envKey: credential.providerEnvKey, + token: input.getValidatedMessagingTokenByEnvKey(input.channels, credential.providerEnvKey), + providerType: staticProviderType ?? MESSAGING_CREDENTIAL_PROVIDER_TYPE, + retainWhileDisabled: staticProviderType !== null, + }; + }) .filter(({ envKey }) => !enabledEnvKeys || enabledEnvKeys.has(envKey)); - const messagingTokenDefs = messagingCredentialDefs.filter( - ({ envKey }) => !disabledEnvKeys.has(envKey), - ); + const messagingTokenDefs: MessagingTokenDef[] = messagingCredentialDefs + .filter(({ envKey }) => !disabledEnvKeys.has(envKey)) + .map(({ retainWhileDisabled: _retainWhileDisabled, ...definition }) => definition); const webSearchEnabled = braveProviderProfile.shouldEnableWebSearch(input.webSearchConfig); const webSearchProvider = webSearch.webSearchProviderForConfig(input.webSearchConfig); @@ -180,10 +188,22 @@ export function prepareCreateSandboxMessaging( const reusableMessagingChannels: string[] = []; if (input.enabledChannels != null) { - for (const { name, envKey, token, providerType } of messagingCredentialDefs) { - if (token) continue; + for (const { + name, + envKey, + token, + providerType, + retainWhileDisabled, + } of messagingCredentialDefs) { const channel = input.getMessagingChannelForEnvKey(envKey); if (!channel || !input.enabledChannels.includes(channel)) continue; + const channelDisabled = disabledChannelNames.has(channel); + if (channelDisabled && !retainWhileDisabled) continue; + // Disabled definitions are intentionally absent from messagingTokenDefs, + // so even a still-readable source token cannot recreate their provider. + // A static credential-bound policy must instead retain the exact gateway + // provider already holding that authority. + if (token && !channelDisabled) continue; const providerReusable = providerType ? input.providerMatchesGatewayCredential(name, providerType, envKey) : requiresExactOpenClawProviderBinding @@ -191,7 +211,7 @@ export function prepareCreateSandboxMessaging( : input.providerExistsInGateway(name); if (!providerReusable) continue; reusableMessagingProviders.push(name); - if (!reusableMessagingChannels.includes(channel)) { + if (!channelDisabled && !reusableMessagingChannels.includes(channel)) { reusableMessagingChannels.push(channel); } } @@ -207,6 +227,7 @@ export function prepareCreateSandboxMessaging( for (const profile of bridgeProfiles) { const channel = profile.channelId; if (!input.enabledChannels.includes(channel)) continue; + if (disabledChannelNames.has(channel)) continue; for (const name of bridgeProviderNamesForChannel(input.sandboxName, channel, [profile])) { if (messagingTokenDefs.some((def) => def.name === name && def.token)) continue; if (reusableMessagingProviders.includes(name)) continue; diff --git a/src/lib/onboard/sandbox-create-intent.ts b/src/lib/onboard/sandbox-create-intent.ts index 757dd4ec0f4..d7372fd70ba 100644 --- a/src/lib/onboard/sandbox-create-intent.ts +++ b/src/lib/onboard/sandbox-create-intent.ts @@ -27,6 +27,27 @@ function filterMessagingProviderRequestsByEnabledChannel( return requests.filter(({ channel }) => !channel || !disabledChannelNames.has(channel)); } +function resolveTokenProviderChannelMap( + requests: readonly SandboxCreateMessagingProviderRequest[], +): Map { + const providerChannels = new Map(); + for (const { channel, name } of requests) { + if (channel) providerChannels.set(name, channel); + } + return providerChannels; +} + +function filterMessagingProvidersByEnabledChannel( + providerNames: string[], + providerChannels: ReadonlyMap, + disabledChannelNames: ReadonlySet, +): string[] { + return providerNames.filter((providerName) => { + const channel = providerChannels.get(providerName); + return !channel || !disabledChannelNames.has(channel); + }); +} + function resolveActiveMessagingChannels({ channels, disabledChannelNames, @@ -136,6 +157,7 @@ export function resolveSandboxCreateIntent({ messagingProviderRequests, disabledChannelNames, ); + const providerChannels = resolveTokenProviderChannelMap(messagingProviderRequests); const activeMessagingChannels = resolveActiveMessagingChannels({ channels, disabledChannelNames, @@ -144,11 +166,11 @@ export function resolveSandboxCreateIntent({ primaryMessagingCredentialEnvKeys, reusableMessagingChannels, }); - // A credential-bound policy can outlive an active channel while the channel is - // stopped, Shields is lowered, or a rebuild replays preserved policy state. - // Keep every exact reusable provider attached to the replacement sandbox; the - // disabled plan still suppresses channel startup, render, and runtime effects. - const enabledReusableMessagingProviders = [...new Set(reusableMessagingProviders)]; + const enabledReusableMessagingProviders = filterMessagingProvidersByEnabledChannel( + [...new Set(reusableMessagingProviders)], + providerChannels, + disabledChannelNames, + ); const normalizedInferenceProvider = inferenceProvider?.trim() || null; diff --git a/src/lib/onboard/sandbox-create-plan-materialization.ts b/src/lib/onboard/sandbox-create-plan-materialization.ts index 709ff7320e6..d59133d8416 100644 --- a/src/lib/onboard/sandbox-create-plan-materialization.ts +++ b/src/lib/onboard/sandbox-create-plan-materialization.ts @@ -192,6 +192,27 @@ export function validateSandboxCreateIntentBindings( }); } +function resolveProviderChannelMap( + requests: readonly SandboxCreateMessagingProviderRequest[], +): Map { + const providerChannels = new Map(); + for (const { channel, name } of requests) { + if (channel) providerChannels.set(name, channel); + } + return providerChannels; +} + +function filterDisabledMessagingProviders( + providerNames: string[], + providerChannels: ReadonlyMap, + disabledChannelNames: ReadonlySet, +): string[] { + return providerNames.filter((providerName) => { + const channel = providerChannels.get(providerName); + return !channel || !disabledChannelNames.has(channel); + }); +} + /** Materialize policy, route metadata, resources, and providers from a secretless intent. */ export function materializeSandboxCreatePlan({ intent, @@ -244,20 +265,20 @@ export function materializeSandboxCreatePlan({ ]; runProviderPreDeleteCleanup(); - // Reusable providers hold gateway-side credential authority. Keep their exact - // attachment on the replacement even when the channel runtime is disabled: - // the initial policy can still carry a credential_binding during rebuild or - // Shields transitions, while enabledMessagingTokenDefs continues to suppress - // new provider creation for disabled channels. - const messagingProviders = [ - ...new Set([ - ...upsertMessagingProviders(enabledMessagingTokenDefs, { - replaceExisting: true, - allowedSandboxes: [intent.sandboxName], - }), - ...intent.reusableMessagingProviders, - ]), - ]; + const providerChannels = resolveProviderChannelMap(intent.messagingProviderRequests); + const messagingProviders = filterDisabledMessagingProviders( + [ + ...new Set([ + ...upsertMessagingProviders(enabledMessagingTokenDefs, { + replaceExisting: true, + allowedSandboxes: [intent.sandboxName], + }), + ...intent.reusableMessagingProviders, + ]), + ], + providerChannels, + new Set(intent.disabledChannelNames), + ); const createProviders = new Set(); if (intent.inferenceProvider) createProviders.add(intent.inferenceProvider); for (const provider of messagingProviders) createProviders.add(provider); diff --git a/src/lib/onboard/sandbox-create-plan.test.ts b/src/lib/onboard/sandbox-create-plan.test.ts index a00616316d3..44d4bba1eb5 100644 --- a/src/lib/onboard/sandbox-create-plan.test.ts +++ b/src/lib/onboard/sandbox-create-plan.test.ts @@ -156,7 +156,7 @@ describe("resolveSandboxCreateIntent", () => { expect(JSON.stringify(requests)).not.toContain("telegram-super-secret"); }); - it("resolves serializable intent and keeps stopped-channel providers attached (#9773)", () => { + it("resolves deterministic serializable intent without execution artifacts", () => { const input = { basePolicyPath: "/repo/policy.yaml", sandboxName: "sandbox", @@ -212,10 +212,7 @@ describe("resolveSandboxCreateIntent", () => { "sandbox-telegram-bridge", "sandbox-slack-bridge", ]); - expect(first.reusableMessagingProviders).toEqual([ - "sandbox-existing-discord", - "sandbox-slack-bridge", - ]); + expect(first.reusableMessagingProviders).toEqual(["sandbox-existing-discord"]); expect(first.extraProviders).toEqual(["custom-provider"]); expect(first.staleExtraProviders).toEqual(["stale-provider"]); expect(first.resourceCreateArgs).toEqual(["--cpu", "4", "--memory", "16Gi"]); @@ -246,22 +243,16 @@ describe("resolveSandboxCreateIntent", () => { expect(JSON.stringify(first)).not.toContain("/tmp/"); }); - it("attaches an exact reusable provider while its channel runtime is stopped (#9773)", () => { + it("attaches a retained static provider while its channel runtime is stopped (#9773)", () => { const intent = resolveSandboxCreateIntent({ basePolicyPath: "/repo/hermes-policy.yaml", sandboxName: "sandbox", channels, enabledChannels: ["discord"], disabledChannelNames: new Set(["discord"]), - messagingProviderRequests: [ - { - name: "sandbox-discord-bridge", - envKey: "DISCORD_BOT_TOKEN", - providerType: "discord-hermes-static-v1", - credentialConfigured: false, - channel: "discord", - }, - ], + // Disabled credential definitions are not provider requests: onboard must + // not create or update their gateway providers during the rebuild. + messagingProviderRequests: [], primaryMessagingCredentialEnvKeys: ["DISCORD_BOT_TOKEN"], reusableMessagingChannels: [], reusableMessagingProviders: ["sandbox-discord-bridge"], @@ -279,14 +270,7 @@ describe("resolveSandboxCreateIntent", () => { const plan = materializeSandboxCreatePlan({ intent, fromRef: "/tmp/Dockerfile", - messagingTokenDefs: [ - { - name: "sandbox-discord-bridge", - envKey: "DISCORD_BOT_TOKEN", - token: null, - providerType: "discord-hermes-static-v1", - }, - ], + messagingTokenDefs: [], runProviderPreDeleteCleanup: vi.fn(), upsertMessagingProviders, getHermesToolGatewayProviderName: vi.fn(), diff --git a/test/hermes-discord-credential-binding.test.ts b/test/hermes-discord-credential-binding.test.ts index 459d0786bc4..8e00988478d 100644 --- a/test/hermes-discord-credential-binding.test.ts +++ b/test/hermes-discord-credential-binding.test.ts @@ -18,6 +18,7 @@ const PROVIDER_TYPE = "discord-hermes-static-v1"; function prepareDiscord( token: string | null, providerMatchesGatewayCredential: () => boolean = () => false, + disabled = false, ) { const discord = listChannels().filter((channel) => channel.name === "discord"); return prepareCreateSandboxMessaging({ @@ -25,7 +26,7 @@ function prepareDiscord( agentName: "hermes", channels: discord, enabledChannels: ["discord"], - disabledChannels: [], + disabledChannels: disabled ? ["discord"] : [], webSearchConfig: null, env: token ? { DISCORD_BOT_TOKEN: token } : {}, getValidatedMessagingTokenByEnvKey: (_channels, envKey) => @@ -74,6 +75,23 @@ describe("Hermes Discord credential endpoint binding", () => { expect(providerMatches).toHaveBeenCalledWith(PROVIDER_NAME, PROVIDER_TYPE, "DISCORD_BOT_TOKEN"); }); + it.each([null, "test-discord-token"])( + "retains the exact Discord provider for a stopped channel with source token %s (#9773)", + (token) => { + const providerMatches = vi.fn(() => true); + const result = prepareDiscord(token, providerMatches, true); + + expect(result.messagingTokenDefs).toEqual([]); + expect(result.reusableMessagingProviders).toEqual([PROVIDER_NAME]); + expect(result.reusableMessagingChannels).toEqual([]); + expect(providerMatches).toHaveBeenCalledWith( + PROVIDER_NAME, + PROVIDER_TYPE, + "DISCORD_BOT_TOKEN", + ); + }, + ); + it("binds Discord REST and WebSocket rewrites to the sandbox provider", () => { const content = loadMessagingChannelPolicyPreset("discord", { agent: "hermes", From a42d03d7c856dc6658b71bc47fee2f330f6f3f51 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sun, 23 Aug 2026 18:30:13 -0700 Subject: [PATCH 6/9] test(e2e): assert retained provider after Hermes rebuild Signed-off-by: Prekshi Vyas --- test/e2e/live/rebuild-hermes.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/e2e/live/rebuild-hermes.test.ts b/test/e2e/live/rebuild-hermes.test.ts index 8976095e791..36fd521a906 100644 --- a/test/e2e/live/rebuild-hermes.test.ts +++ b/test/e2e/live/rebuild-hermes.test.ts @@ -1225,6 +1225,19 @@ test(STALE_BASE_REBUILD /Hermes gateway (?:restarted and verified|recovered) after state restore/u, ); await waitForSandboxReady(host, apiKey, activeOpenshellBin, "phase-6-post-rebuild"); + const rebuiltProviderAttachments = await host.command( + activeOpenshellBin, + ["sandbox", "provider", "list", "-g", "nemoclaw", SANDBOX_NAME], + { + artifactName: "phase-6-post-rebuild-provider-attachments", + env: testEnv(apiKey), + redactionValues, + timeoutMs: OPENSHELL_TIMEOUT_MS, + }, + ); + expectExitZero(rebuiltProviderAttachments, "list rebuilt Hermes provider attachments"); + const rebuiltProviderNames = resultText(rebuiltProviderAttachments).split(/\s+/u); + expect(rebuiltProviderNames).toContain(`${SANDBOX_NAME}-discord-bridge`); const backupPathText = rebuildOutput.match(/^\s*Backup:\s+(.+)$/mu)?.[1]?.trim(); const rebuildBackupPath = backupPathText From c162cb188cee8a0d76d7156e7daf86fc64afd21e Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sun, 23 Aug 2026 18:34:36 -0700 Subject: [PATCH 7/9] test(e2e): reuse provider attachment fixture Signed-off-by: Prekshi Vyas --- test/e2e/live/rebuild-hermes.test.ts | 29 ++++++++-------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/test/e2e/live/rebuild-hermes.test.ts b/test/e2e/live/rebuild-hermes.test.ts index 36fd521a906..9117516d499 100644 --- a/test/e2e/live/rebuild-hermes.test.ts +++ b/test/e2e/live/rebuild-hermes.test.ts @@ -14,6 +14,7 @@ import { assertExitZero as expectExitZero } from "../fixtures/clients/command.ts import { type HostCliClient, resultText } from "../fixtures/clients/index.ts"; import { validateSandboxName } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; +import { expectSandboxProviderAttachment } from "../fixtures/gateway-providers.ts"; import { readJsonFileOr, restoreFile, @@ -276,7 +277,6 @@ function testEnv(apiKey?: string, extra: NodeJS.ProcessEnv = {}): NodeJS.Process function fail(message: string): never { throw new Error(message); } - function expectedHermesVersion(): string { const manifest = fs.readFileSync(HERMES_MANIFEST, "utf8"); const match = manifest.match(/^expected_version:\s*"?([^"\n]+)"?/m); @@ -286,15 +286,6 @@ function expectedHermesVersion(): string { return match![1].trim(); } -function expectEqual(actual: string | undefined, expected: string, message: string): void { - switch (actual === expected) { - case true: - return; - default: - throw new Error(message); - } -} - async function bestEffortPrecleanHermesResources( host: HostCliClient, apiKey: string | undefined, @@ -1225,19 +1216,16 @@ test(STALE_BASE_REBUILD /Hermes gateway (?:restarted and verified|recovered) after state restore/u, ); await waitForSandboxReady(host, apiKey, activeOpenshellBin, "phase-6-post-rebuild"); - const rebuiltProviderAttachments = await host.command( - activeOpenshellBin, - ["sandbox", "provider", "list", "-g", "nemoclaw", SANDBOX_NAME], + await expectSandboxProviderAttachment( + sandbox, + SANDBOX_NAME, + `${SANDBOX_NAME}-discord-bridge`, + "present", { artifactName: "phase-6-post-rebuild-provider-attachments", env: testEnv(apiKey), - redactionValues, - timeoutMs: OPENSHELL_TIMEOUT_MS, }, ); - expectExitZero(rebuiltProviderAttachments, "list rebuilt Hermes provider attachments"); - const rebuiltProviderNames = resultText(rebuiltProviderAttachments).split(/\s+/u); - expect(rebuiltProviderNames).toContain(`${SANDBOX_NAME}-discord-bridge`); const backupPathText = rebuildOutput.match(/^\s*Backup:\s+(.+)$/mu)?.[1]?.trim(); const rebuildBackupPath = backupPathText @@ -1314,11 +1302,10 @@ test(STALE_BASE_REBUILD expectExitZero(hermesVersion, "Hermes version after rebuild"); const hermesVersionText = resultText(hermesVersion); const actualHermesVersion = hermesVersionText.match(/v(\d+\.\d+\.\d+)/)?.[1]; - expectEqual( + expect( actualHermesVersion, - expectedVersion, `Hermes version output did not include expected release ${expectedVersion}: ${hermesVersionText}`, - ); + ).toBe(expectedVersion); await cronRestore.verify(rebuildOutput, rebuildBackupPath); await cronRestore.verifyStrandedGateRecovery(); const restoredKanbanDatabase = await host.command( From d36581135970aa072ce3c567197d51b485a643fd Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sun, 23 Aug 2026 18:48:18 -0700 Subject: [PATCH 8/9] fix(ci): scope docs flags to NemoClaw examples Signed-off-by: Prekshi Vyas --- test/e2e/e2e-cloud-experimental/check-docs.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/e2e/e2e-cloud-experimental/check-docs.sh b/test/e2e/e2e-cloud-experimental/check-docs.sh index 197abb399e8..78ba340e49d 100755 --- a/test/e2e/e2e-cloud-experimental/check-docs.sh +++ b/test/e2e/e2e-cloud-experimental/check-docs.sh @@ -353,9 +353,23 @@ JSON _doc_flags="$( printf '%s\n' "$_section" \ | LC_ALL=C perl -CS -ne ' - if (/^```/) { $in_fence = !$in_fence; next; } + sub emit_flags { + my ($line) = @_; + while ($line =~ /--([a-z][a-z0-9-]+)/g) { print "--$1\n"; } + } + + if (/^```/) { + $in_fence = !$in_fence; + $in_nemoclaw_command = 0; + next; + } if ($in_fence) { - while (/--([a-z][a-z0-9-]+)/g) { print "--$1\n"; } + # Ignore flags belonging to shell tools shown alongside the + # CLI, while preserving flags on multiline NemoClaw examples. + if ($in_nemoclaw_command || /(?:^|\s)(?:\$\$)?nemoclaw(?:\s|$)/) { + emit_flags($_); + $in_nemoclaw_command = /\\\s*$/ ? 1 : 0; + } } else { while (/`--([a-z][a-z0-9-]+)/g) { print "--$1\n"; } } From 40a051018923a2337baeff2507019291df41296c Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sun, 23 Aug 2026 19:04:49 -0700 Subject: [PATCH 9/9] fix(ci): restore PR validation guards Signed-off-by: Prekshi Vyas --- test/cli-coverage-sequencer.test.ts | 6 +++--- test/e2e/live/rebuild-hermes.test.ts | 11 +++++------ test/helpers/cli-coverage-sequencer.ts | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/test/cli-coverage-sequencer.test.ts b/test/cli-coverage-sequencer.test.ts index c7d864235f1..a89dbdcb1ef 100644 --- a/test/cli-coverage-sequencer.test.ts +++ b/test/cli-coverage-sequencer.test.ts @@ -117,9 +117,9 @@ describe("stable CLI coverage sharding", () => { expect(Object.fromEntries(owners)).toEqual({ "cli:src/lib/example.test.ts": 6, "e2e-support:test/e2e/support/example.test.ts": 8, - "integration:test/hermes-restart-config-seal-write-lock.test.ts": 6, - "integration:test/local-credential-helper-fields.test.ts": 7, - "integration:test/regular-0.test.ts": 6, + "integration:test/hermes-restart-config-seal-write-lock.test.ts": 8, + "integration:test/local-credential-helper-fields.test.ts": 3, + "integration:test/regular-0.test.ts": 8, }); }); diff --git a/test/e2e/live/rebuild-hermes.test.ts b/test/e2e/live/rebuild-hermes.test.ts index 9117516d499..388635c0cf0 100644 --- a/test/e2e/live/rebuild-hermes.test.ts +++ b/test/e2e/live/rebuild-hermes.test.ts @@ -6,6 +6,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; +import { loadAgent } from "../../../src/lib/agent/defs"; import { shellQuote } from "../../../src/lib/core/shell-quote"; import { readSandboxBaseImageResolutionMetadata } from "../../../src/lib/sandbox-base-image"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; @@ -80,7 +81,6 @@ process.env.NEMOCLAW_CLI_BIN ??= CLI_ENTRYPOINT; // local NemoClaw registry/session state, and `nemoclaw rebuild --yes`. // Literal interactive issue #3025 reproduction paths (`hermes rebuild`, modal // prompt, and `Y` confirmation) remain outside this Vitest migration. -const HERMES_MANIFEST = path.join(REPO_ROOT, "agents", "hermes", "manifest.yaml"); const OLD_HERMES_VERSION = `v${REBUILD_HERMES_OLD_BASE_FIXTURE.hermesCalver}`; const OLD_HERMES_REGISTRY_VERSION = OLD_HERMES_VERSION.slice(1); const STALE_BASE_REBUILD = process.env.NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E === "1"; @@ -277,13 +277,12 @@ function testEnv(apiKey?: string, extra: NodeJS.ProcessEnv = {}): NodeJS.Process function fail(message: string): never { throw new Error(message); } + function expectedHermesVersion(): string { - const manifest = fs.readFileSync(HERMES_MANIFEST, "utf8"); - const match = manifest.match(/^expected_version:\s*"?([^"\n]+)"?/m); - expect(match?.[1], `Could not parse expected Hermes version from ${HERMES_MANIFEST}`).toEqual( - expect.any(String), + return ( + loadAgent("hermes").expectedVersion ?? + fail("Hermes manifest must declare expected_version for live rebuild coverage") ); - return match![1].trim(); } async function bestEffortPrecleanHermesResources( diff --git a/test/helpers/cli-coverage-sequencer.ts b/test/helpers/cli-coverage-sequencer.ts index 1d979dc7751..a86830cdec9 100644 --- a/test/helpers/cli-coverage-sequencer.ts +++ b/test/helpers/cli-coverage-sequencer.ts @@ -42,7 +42,7 @@ const cliCoverageProjects = new Set(["cli", "integration", "e2e-support"]); // Integration coverage is serialized, so it needs an independent salt instead // of relying on combined weight from the parallel CLI and E2E-support lanes. const stableShardSalt = "7257"; -const integrationShardSalt = "28320"; +const integrationShardSalt = "6941"; const e2eSupportShardSalt = "13930"; // Only measured outliers are stored; new and ordinary files share the // conservative fallback used to estimate each stable shard's load.