mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix: propagate AWS SDK auth sentinel for IMDS/instance role Bedrock auth (#68964)
* fix: propagate AWS SDK auth sentinel for IMDS/instance role Bedrock auth When Bedrock auth resolves via AWS SDK default credential chain (IMDS, ECS task role) with no explicit API key, the auth controller returned early without calling setRuntimeApiKey(). This left pi's authStorage unaware that the provider is authenticated, causing 'No API key found for amazon-bedrock' errors. Now, when mode is 'aws-sdk' and no explicit API key is available: 1. Try prepareProviderRuntimeAuth to resolve runtime credentials 2. If that returns a real apiKey, use it with auth refresh scheduling 3. Otherwise inject a '__aws_sdk_auth__' sentinel so pi's hasConfiguredAuth() passes and the AWS SDK handles request signing This is a focused fix in auth-controller.ts only, avoiding the risky model-auth-runtime-shared.ts changes that could re-introduce the fake-apiKey injection pattern on ECS (see prior regressions #49891, #50699, #54274). Fixes #62995 * fix(pi-auth): clean up aws-sdk sentinel fallback * docs(changelog): note aws-sdk Bedrock auth fix --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/Pi auth: preserve AWS SDK-authenticated Bedrock runs for IMDS and task-role setups, clear stale refresh timers on sentinel fallback, and log unexpected runtime-auth prep failures instead of silently leaving the provider unauthenticated. Thanks @wirjo.
|
||||
- Config/gateway: recover configs accidentally prefixed with non-JSON output during gateway startup or `openclaw doctor --fix`, preserving the clobbered file as a backup while leaving normal config reads read-only.
|
||||
- Agents/GitHub Copilot: normalize connection-bound Responses item IDs in the Copilot provider wrapper so replayed histories no longer fail after the upstream connection changes. (#69362) Thanks @Menci.
|
||||
- Pi embedded runs: pass real built-in tools into Pi session creation and then narrow active tool names after custom tool registration, so the runner and compaction paths compile cleanly and keep OpenClaw-managed custom tool allowlists without feeding string arrays into `createAgentSession`. Thanks @vincentkoc.
|
||||
|
||||
@@ -88,6 +88,7 @@ function createMutableEmbeddedRunAuthController(params: {
|
||||
harness: MutableAuthControllerHarness;
|
||||
setRuntimeApiKey: RuntimeApiKeySetter;
|
||||
profileCandidates?: string[];
|
||||
warn?: (message: string) => void;
|
||||
}) {
|
||||
return createEmbeddedRunAuthController({
|
||||
config: undefined,
|
||||
@@ -135,7 +136,7 @@ function createMutableEmbeddedRunAuthController(params: {
|
||||
log: {
|
||||
debug: () => undefined,
|
||||
info: () => undefined,
|
||||
warn: () => undefined,
|
||||
warn: params.warn ?? (() => undefined),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -372,4 +373,126 @@ describe("createEmbeddedRunAuthController", () => {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
describe("aws-sdk auth without explicit API key (IMDS / instance role)", () => {
|
||||
it("injects runtime auth when prepareProviderRuntimeAuth resolves credentials", async () => {
|
||||
const harness = createMutableAuthControllerHarness();
|
||||
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
|
||||
|
||||
mocks.getApiKeyForModel.mockResolvedValue({
|
||||
apiKey: undefined,
|
||||
mode: "aws-sdk",
|
||||
source: "aws-sdk default chain",
|
||||
});
|
||||
mocks.prepareProviderRuntimeAuth.mockResolvedValue({
|
||||
apiKey: "imds-runtime-token",
|
||||
expiresAt: Date.now() + 3600_000,
|
||||
});
|
||||
|
||||
const controller = createMutableEmbeddedRunAuthController({
|
||||
harness,
|
||||
setRuntimeApiKey,
|
||||
profileCandidates: [undefined as unknown as string],
|
||||
});
|
||||
|
||||
await controller.initializeAuthProfile();
|
||||
|
||||
expect(setRuntimeApiKey).toHaveBeenCalledWith("custom-openai", "imds-runtime-token");
|
||||
expect(harness.runtimeAuthState).toMatchObject({
|
||||
sourceApiKey: "__aws_sdk_auth__",
|
||||
authMode: "aws-sdk",
|
||||
});
|
||||
expect(harness.runtimeAuthState?.expiresAt).toBeGreaterThan(Date.now());
|
||||
controller.stopRuntimeAuthRefreshTimer();
|
||||
});
|
||||
|
||||
it("injects sentinel when prepareProviderRuntimeAuth returns no apiKey", async () => {
|
||||
const harness = createMutableAuthControllerHarness();
|
||||
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
|
||||
|
||||
mocks.getApiKeyForModel.mockResolvedValue({
|
||||
apiKey: undefined,
|
||||
mode: "aws-sdk",
|
||||
source: "aws-sdk default chain",
|
||||
});
|
||||
mocks.prepareProviderRuntimeAuth.mockResolvedValue(null);
|
||||
|
||||
const controller = createMutableEmbeddedRunAuthController({
|
||||
harness,
|
||||
setRuntimeApiKey,
|
||||
profileCandidates: [undefined as unknown as string],
|
||||
});
|
||||
|
||||
await controller.initializeAuthProfile();
|
||||
|
||||
expect(setRuntimeApiKey).toHaveBeenCalledWith("custom-openai", "__aws_sdk_auth__");
|
||||
expect(harness.runtimeAuthState).toBeNull();
|
||||
});
|
||||
|
||||
it("clears any stale refresh timer before sentinel injection", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const harness = createMutableAuthControllerHarness();
|
||||
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
|
||||
|
||||
harness.runtimeAuthState = {
|
||||
generation: 1,
|
||||
sourceApiKey: "__aws_sdk_auth__",
|
||||
authMode: "aws-sdk",
|
||||
refreshTimer: setTimeout(() => undefined, 60_000),
|
||||
};
|
||||
|
||||
mocks.getApiKeyForModel.mockResolvedValue({
|
||||
apiKey: undefined,
|
||||
mode: "aws-sdk",
|
||||
source: "aws-sdk default chain",
|
||||
});
|
||||
mocks.prepareProviderRuntimeAuth.mockResolvedValue(null);
|
||||
|
||||
const controller = createMutableEmbeddedRunAuthController({
|
||||
harness,
|
||||
setRuntimeApiKey,
|
||||
profileCandidates: [undefined as unknown as string],
|
||||
});
|
||||
|
||||
await controller.initializeAuthProfile();
|
||||
|
||||
expect(setRuntimeApiKey).toHaveBeenCalledWith("custom-openai", "__aws_sdk_auth__");
|
||||
expect(harness.runtimeAuthState).toBeNull();
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("injects sentinel when prepareProviderRuntimeAuth throws", async () => {
|
||||
const harness = createMutableAuthControllerHarness();
|
||||
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
|
||||
const warn = vi.fn<(message: string) => void>();
|
||||
|
||||
mocks.getApiKeyForModel.mockResolvedValue({
|
||||
apiKey: undefined,
|
||||
mode: "aws-sdk",
|
||||
source: "aws-sdk default chain",
|
||||
});
|
||||
mocks.prepareProviderRuntimeAuth.mockRejectedValue(new Error("No runtime auth plugin"));
|
||||
|
||||
const controller = createMutableEmbeddedRunAuthController({
|
||||
harness,
|
||||
setRuntimeApiKey,
|
||||
profileCandidates: [undefined as unknown as string],
|
||||
warn,
|
||||
});
|
||||
|
||||
await controller.initializeAuthProfile();
|
||||
|
||||
expect(setRuntimeApiKey).toHaveBeenCalledWith("custom-openai", "__aws_sdk_auth__");
|
||||
expect(harness.runtimeAuthState).toBeNull();
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"prepareProviderRuntimeAuth failed for custom-openai, falling back to sentinel: No runtime auth plugin",
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -368,6 +368,50 @@ export function createEmbeddedRunAuthController(params: {
|
||||
`No API key resolved for provider "${runtimeModel.provider}" (auth mode: ${apiKeyInfo.mode}).`,
|
||||
);
|
||||
}
|
||||
// AWS SDK auth via IMDS / instance role / ECS task role: no explicit API
|
||||
// key is available but the SDK default credential chain can resolve
|
||||
// credentials at runtime. We must still call setRuntimeApiKey so that
|
||||
// pi's authStorage considers the provider authenticated. Try
|
||||
// prepareProviderRuntimeAuth first (it can sign requests and return a
|
||||
// short-lived token); fall back to a sentinel value when the provider
|
||||
// plugin does not implement runtime auth preparation.
|
||||
const runtimeModel = params.getRuntimeModel();
|
||||
const AWS_SDK_AUTH_SENTINEL = "__aws_sdk_auth__";
|
||||
try {
|
||||
const preparedAuth = await prepareRuntimeAuthForModel({
|
||||
runtimeModel,
|
||||
apiKey: AWS_SDK_AUTH_SENTINEL,
|
||||
authMode: apiKeyInfo.mode,
|
||||
profileId: apiKeyInfo.profileId,
|
||||
});
|
||||
applyPreparedRuntimeRequestOverrides({ runtimeModel, preparedAuth: preparedAuth ?? {} });
|
||||
if (preparedAuth?.apiKey) {
|
||||
clearRuntimeAuthRefreshTimer();
|
||||
params.authStorage.setRuntimeApiKey(runtimeModel.provider, preparedAuth.apiKey);
|
||||
params.setRuntimeAuthState({
|
||||
generation: nextRuntimeAuthGeneration(),
|
||||
sourceApiKey: AWS_SDK_AUTH_SENTINEL,
|
||||
authMode: apiKeyInfo.mode,
|
||||
profileId: resolvedProfileId,
|
||||
expiresAt: preparedAuth.expiresAt,
|
||||
});
|
||||
if (preparedAuth.expiresAt) {
|
||||
scheduleRuntimeAuthRefresh();
|
||||
}
|
||||
params.setLastProfileId(resolvedProfileId);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
params.log.warn(
|
||||
`prepareProviderRuntimeAuth failed for ${runtimeModel.provider}, falling back to sentinel: ${formatErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
// No runtime auth plugin resolved a real credential. Inject the
|
||||
// sentinel so pi's hasConfiguredAuth() passes and the AWS SDK default
|
||||
// credential chain handles actual request signing.
|
||||
clearRuntimeAuthRefreshTimer();
|
||||
params.authStorage.setRuntimeApiKey(runtimeModel.provider, AWS_SDK_AUTH_SENTINEL);
|
||||
params.setRuntimeAuthState(null);
|
||||
params.setLastProfileId(resolvedProfileId);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user