mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(microsoft-foundry): bound Entra token cache
This commit is contained in:
@@ -126,6 +126,7 @@ const defaultFoundryModelId = "gpt-5.4";
|
||||
const defaultFoundryProfileId = "microsoft-foundry:entra";
|
||||
const defaultFoundryAgentDir = "/tmp/test-agent";
|
||||
const defaultAzureCliLoginError = "Please run 'az login' to setup account.";
|
||||
const foundryTokenCacheMaxEntries = 128;
|
||||
let runtimeAuthTestSequence = 0;
|
||||
let runtimeAuthTestTenantId = "tenant-0";
|
||||
|
||||
@@ -615,6 +616,34 @@ describe("microsoft-foundry plugin", () => {
|
||||
expect(requireRuntimeAuthResult(second).apiKey).toBe("deduped-token");
|
||||
});
|
||||
|
||||
it("bounds settled Entra tokens by least-recently-used account tuple", async () => {
|
||||
const provider = registerProvider();
|
||||
const prepareRuntimeAuth = requirePrepareRuntimeAuth(provider);
|
||||
execFileMock.mockImplementation(async () => ({
|
||||
stdout: JSON.stringify({
|
||||
accessToken: `token-${execFileMock.mock.calls.length}`,
|
||||
expiresOn: new Date(Date.now() + 10 * 60_000).toISOString(),
|
||||
}),
|
||||
stderr: "",
|
||||
}));
|
||||
const prepareForTenant = async (tenantId: string) => {
|
||||
ensureAuthProfileStoreMock.mockReturnValueOnce(buildEntraProfileStore({ tenantId }));
|
||||
return await prepareRuntimeAuth(buildFoundryRuntimeAuthContext());
|
||||
};
|
||||
|
||||
for (let index = 0; index < foundryTokenCacheMaxEntries; index += 1) {
|
||||
await prepareForTenant(`lru-${runtimeAuthTestTenantId}-${index}`);
|
||||
}
|
||||
expect(execFileMock).toHaveBeenCalledTimes(foundryTokenCacheMaxEntries);
|
||||
|
||||
await prepareForTenant(`lru-${runtimeAuthTestTenantId}-0`);
|
||||
expect(execFileMock).toHaveBeenCalledTimes(foundryTokenCacheMaxEntries);
|
||||
|
||||
await prepareForTenant(`lru-${runtimeAuthTestTenantId}-${foundryTokenCacheMaxEntries}`);
|
||||
await prepareForTenant(`lru-${runtimeAuthTestTenantId}-1`);
|
||||
expect(execFileMock).toHaveBeenCalledTimes(foundryTokenCacheMaxEntries + 2);
|
||||
});
|
||||
|
||||
it("clears failed refresh state so later concurrent retries succeed", async () => {
|
||||
const provider = registerProvider();
|
||||
const prepareRuntimeAuth = requirePrepareRuntimeAuth(provider);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Microsoft Foundry plugin module implements runtime behavior.
|
||||
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
||||
import type {
|
||||
ProviderPreparedRuntimeAuth,
|
||||
ProviderPrepareRuntimeAuthContext,
|
||||
@@ -27,6 +28,9 @@ import {
|
||||
const cachedTokens = new Map<string, CachedTokenEntry>();
|
||||
const refreshPromises = new Map<string, Promise<{ apiKey: string; expiresAt: number }>>();
|
||||
const FOUNDRY_TOKEN_FALLBACK_LIFETIME_MS = 55 * 60 * 1000;
|
||||
// Bound settled credential material across profile generations. In-flight
|
||||
// refresh ownership remains separate so admission never evicts active work.
|
||||
const FOUNDRY_TOKEN_CACHE_MAX_ENTRIES = 128;
|
||||
|
||||
async function refreshEntraToken(params?: {
|
||||
scope?: string;
|
||||
@@ -40,10 +44,18 @@ async function refreshEntraToken(params?: {
|
||||
asDateTimestampMs(rawExpiry) ??
|
||||
resolveExpiresAtMsFromDurationMs(FOUNDRY_TOKEN_FALLBACK_LIFETIME_MS, { nowMs: now }) ??
|
||||
now;
|
||||
cachedTokens.set(getFoundryTokenCacheKey(params), {
|
||||
for (const [cacheKey, cachedToken] of cachedTokens) {
|
||||
if (cachedToken.expiresAt <= now) {
|
||||
cachedTokens.delete(cacheKey);
|
||||
}
|
||||
}
|
||||
const cacheKey = getFoundryTokenCacheKey(params);
|
||||
cachedTokens.delete(cacheKey);
|
||||
cachedTokens.set(cacheKey, {
|
||||
token: result.accessToken,
|
||||
expiresAt,
|
||||
});
|
||||
pruneMapToMaxSize(cachedTokens, FOUNDRY_TOKEN_CACHE_MAX_ENTRIES);
|
||||
return { apiKey: result.accessToken, expiresAt };
|
||||
}
|
||||
|
||||
@@ -107,6 +119,9 @@ export async function prepareFoundryRuntimeAuth(
|
||||
const refreshAfterMs =
|
||||
resolveExpiresAtMsFromDurationMs(TOKEN_REFRESH_MARGIN_MS, { nowMs: now }) ?? now;
|
||||
if (cachedToken && hasValidClock && cachedToken.expiresAt > refreshAfterMs) {
|
||||
// Map insertion order is the eviction order; touch valid hits to retain active accounts.
|
||||
cachedTokens.delete(cacheKey);
|
||||
cachedTokens.set(cacheKey, cachedToken);
|
||||
return {
|
||||
apiKey: cachedToken.token,
|
||||
expiresAt: cachedToken.expiresAt,
|
||||
@@ -116,6 +131,7 @@ export async function prepareFoundryRuntimeAuth(
|
||||
},
|
||||
};
|
||||
}
|
||||
cachedTokens.delete(cacheKey);
|
||||
let refreshPromise = refreshPromises.get(cacheKey);
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = refreshEntraToken({
|
||||
|
||||
Reference in New Issue
Block a user