From da8993203c669e5666c28ea5cf40e1441092d517 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 22 Apr 2026 23:41:27 -0700 Subject: [PATCH] fix(amazon-bedrock): refresh live discovery and guardrail config --- CHANGELOG.md | 1 + extensions/amazon-bedrock/index.test.ts | 132 +++++++++++++++++- .../amazon-bedrock/register.sync.runtime.ts | 28 ++-- 3 files changed, 150 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d720884aa23..c14252a5ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,7 @@ Docs: https://docs.openclaw.ai - GitHub Copilot: re-read plugin discovery config from the live runtime snapshot, so toggling `plugins.entries.github-copilot.config.discovery.enabled` takes effect without a restart. Thanks @vincentkoc. - Ollama: re-read plugin discovery config from the live runtime snapshot, so toggling `plugins.entries.ollama.config.discovery.enabled` takes effect without a restart. Thanks @vincentkoc. - OpenAI: re-read the plugin prompt-overlay personality from live runtime config, so GPT-5 system prompt contributions update without a restart when `plugins.entries.openai.config.personality` changes. Thanks @vincentkoc. +- Amazon Bedrock: re-read live discovery and guardrail plugin config, so toggling `plugins.entries.amazon-bedrock.config.discovery` or `plugins.entries.amazon-bedrock.config.guardrail` takes effect without a restart. Thanks @vincentkoc. - Agents/subagents: drop bare `NO_REPLY` from the parent turn when the session still has pending spawned children, so direct-conversation surfaces such as Telegram DMs no longer rewrite the sentinel into visible fallback chatter while waiting for the child completion event. (#69942) Thanks @neeravmakwana. - Plugins/install: keep bundled plugin dependencies off npm install while repairing them when plugins activate from a packaged install, including Feishu/Lark, Browser, and direct bundled channel setup-entry loads. - CLI/channels: skip and cache bundled channel plugin, setup, and secrets load failures during read-only discovery, so one broken unused bundled channel cannot crash `openclaw status` or bootstrap secret scans. diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index 2ce44f04a63..532b2757e36 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -7,9 +7,15 @@ import type { PluginRuntime } from "../../src/plugins/runtime/types.js"; import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js"; import amazonBedrockPlugin from "./index.js"; -type InferenceProfileResult = { models?: Array<{ modelArn?: string }> } | Error; +type BedrockClientResult = + | { + models?: Array<{ modelArn?: string }>; + modelSummaries?: Array>; + inferenceProfileSummaries?: Array>; + } + | Error; -const inferenceProfileResults: InferenceProfileResult[] = []; +const inferenceProfileResults: BedrockClientResult[] = []; const bedrockClientConfigs: Array> = []; const sendGetInferenceProfile = vi.fn(async () => { const next = inferenceProfileResults.shift(); @@ -24,6 +30,14 @@ vi.mock("@aws-sdk/client-bedrock", () => { constructor(readonly input: { inferenceProfileIdentifier: string }) {} } + class ListFoundationModelsCommand { + constructor(readonly input: Record = {}) {} + } + + class ListInferenceProfilesCommand { + constructor(readonly input: Record = {}) {} + } + class BedrockClient { constructor(config: Record = {}) { bedrockClientConfigs.push(config); @@ -35,6 +49,8 @@ vi.mock("@aws-sdk/client-bedrock", () => { return { BedrockClient, GetInferenceProfileCommand, + ListFoundationModelsCommand, + ListInferenceProfilesCommand, }; }); @@ -113,10 +129,12 @@ function callWrappedStream( provider: RegisteredProviderPlugin, modelId: string, modelDescriptor: never, + config?: OpenClawConfig, ): Record { const wrapped = provider.wrapStreamFn?.({ provider: "amazon-bedrock", modelId, + config, streamFn: spyStreamFn, } as never); @@ -138,6 +156,31 @@ function callWrappedStream( return result; } +async function runCatalog( + provider: RegisteredProviderPlugin, + config: OpenClawConfig, + env: NodeJS.ProcessEnv = {} as NodeJS.ProcessEnv, +) { + return provider.catalog?.run({ + config, + env, + } as never); +} + +function runtimePluginConfig(config?: Record): OpenClawConfig { + return { + plugins: { + entries: config + ? { + "amazon-bedrock": { + config, + }, + } + : {}, + }, + } as OpenClawConfig; +} + describe("amazon-bedrock provider plugin", () => { beforeEach(() => { inferenceProfileResults.length = 0; @@ -354,6 +397,91 @@ describe("amazon-bedrock provider plugin", () => { // Non-Anthropic models should also get cacheRetention: "none" expect(result).toMatchObject({ cacheRetention: "none" }); }); + + it("uses live plugin config to inject guardrailConfig after startup disable", async () => { + const provider = await registerWithConfig(undefined); + const result = callWrappedStream( + provider, + NON_ANTHROPIC_MODEL, + MODEL_DESCRIPTOR, + runtimePluginConfig({ + guardrail: { + guardrailIdentifier: "live-guardrail", + guardrailVersion: "7", + }, + }), + ); + + expect(result._capturedPayload).toEqual({ + guardrailConfig: { + guardrailIdentifier: "live-guardrail", + guardrailVersion: "7", + }, + }); + }); + + it("does not revive startup guardrail config when the live plugin entry is removed", async () => { + const provider = await registerWithConfig({ + guardrail: { + guardrailIdentifier: "startup-guardrail", + guardrailVersion: "5", + }, + }); + const result = callWrappedStream( + provider, + NON_ANTHROPIC_MODEL, + MODEL_DESCRIPTOR, + runtimePluginConfig(undefined), + ); + + expect(result).not.toHaveProperty("_capturedPayload"); + expect(result).toMatchObject({ cacheRetention: "none" }); + }); + }); + + describe("discovery config", () => { + it("uses live plugin config to re-enable discovery after startup disable", async () => { + inferenceProfileResults.push( + { + modelSummaries: [ + { + modelId: NON_ANTHROPIC_MODEL, + modelName: "Nova Micro", + providerName: "Amazon", + inputModalities: ["TEXT"], + outputModalities: ["TEXT"], + responseStreamingSupported: true, + modelLifecycle: { status: "ACTIVE" }, + }, + ], + }, + { + inferenceProfileSummaries: [], + }, + ); + const provider = await registerWithConfig({ + discovery: { + enabled: false, + }, + }); + + const catalog = await runCatalog( + provider, + runtimePluginConfig({ + discovery: { + enabled: true, + region: "us-east-1", + }, + }), + ); + + expect(catalog).toMatchObject({ + provider: { + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + api: "bedrock-converse-stream", + }, + }); + }); }); describe("application inference profile cache point injection", () => { diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index f4981f21173..60c0f9900b7 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -1,4 +1,5 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; +import { resolvePluginConfigObject } from "openclaw/plugin-sdk/config-runtime"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; import { ANTHROPIC_BY_MODEL_REPLAY_HOOKS, @@ -249,8 +250,17 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { /ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i, ] as const; const anthropicByModelReplayHooks = ANTHROPIC_BY_MODEL_REPLAY_HOOKS; - const pluginConfig = (api.pluginConfig ?? {}) as AmazonBedrockPluginConfig; - const guardrail = pluginConfig.guardrail; + const startupPluginConfig = (api.pluginConfig ?? {}) as AmazonBedrockPluginConfig; + + function resolveCurrentPluginConfig( + config: { plugins?: { entries?: Record } } | undefined, + ): AmazonBedrockPluginConfig | undefined { + const runtimePluginConfig = resolvePluginConfigObject(config, providerId); + return ( + (runtimePluginConfig as AmazonBedrockPluginConfig | undefined) ?? + (config ? undefined : startupPluginConfig) + ); + } api.registerMemoryEmbeddingProvider(bedrockMemoryEmbeddingProviderAdapter); @@ -266,11 +276,6 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { return createBedrockNoCacheWrapper(streamFn); }; - const cacheWrapStreamFn = - guardrail?.guardrailIdentifier && guardrail?.guardrailVersion - ? createGuardrailWrapStreamFn(baseWrapStreamFn, guardrail) - : baseWrapStreamFn; - /** Extract the AWS region from a bedrock-runtime baseUrl. */ function extractRegionFromBaseUrl(baseUrl: string | undefined): string | undefined { if (!baseUrl) { @@ -321,9 +326,10 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { catalog: { order: "simple", run: async (ctx) => { + const currentPluginConfig = resolveCurrentPluginConfig(ctx.config); const implicit = await resolveImplicitBedrockProvider({ config: ctx.config, - pluginConfig, + pluginConfig: currentPluginConfig, env: ctx.env, }); if (!implicit) { @@ -340,8 +346,12 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { resolveConfigApiKey: ({ env }) => resolveBedrockConfigApiKey(env), ...anthropicByModelReplayHooks, wrapStreamFn: ({ modelId, config, model, streamFn }) => { + const currentGuardrail = resolveCurrentPluginConfig(config)?.guardrail; // Apply cache + guardrail wrapping. - const wrapped = cacheWrapStreamFn({ modelId, streamFn }); + const wrapped = + currentGuardrail?.guardrailIdentifier && currentGuardrail?.guardrailVersion + ? createGuardrailWrapStreamFn(baseWrapStreamFn, currentGuardrail)({ modelId, streamFn }) + : baseWrapStreamFn({ modelId, streamFn }); const region = resolveBedrockRegion(config) ?? extractRegionFromBaseUrl(model?.baseUrl); const mayNeedCacheInjection = isBedrockAppInferenceProfile(modelId) && !piAiWouldInjectCachePoints(modelId);