fix(microsoft-foundry): bound Entra token cache

This commit is contained in:
Amp
2026-08-21 14:14:48 +00:00
parent 75c44b2b98
commit 8c6b8537e4
2 changed files with 46 additions and 1 deletions
@@ -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);
+17 -1
View File
@@ -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({