Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, mock, test } from "bun:test";
import { AuthProfilesManager } from "../auth-profiles-manager.js";

/** Probes the bounded-cache invariant: even when the gateway sees many
* distinct one-shot agents, the agentOwner/agentOrg caches must not grow
* unbounded. Without the cap they would accumulate one entry per distinct
* agentId for the pod's lifetime (the existing TTL only refreshes values,
* not map size). Cap is 1024 — exercised at 2048 lookups. */
describe("AuthProfilesManager: bounded auth-resolver caches", () => {
test("agentOwner cache stays bounded under many distinct one-shot lookups", async () => {
const manager = new AuthProfilesManager({
ephemeralProfiles: { get: () => undefined } as never,
declaredAgents: { get: () => undefined } as never,
userAuthProfiles: { list: mock(async () => []) } as never,
secretStore: { get: mock(async () => undefined) } as never,
agentOwnerResolver: async (id) => `owner-${id}`,
agentOrgResolver: async (id) => `org-${id}`,
});

// 2048 distinct agentIds — twice the cap.
for (let i = 0; i < 2048; i++) {
// @ts-expect-error — exercising private resolver path via friend access
await manager["resolveAgentOwnerUserId"](`agent-${i}`);
// @ts-expect-error
await manager["resolveAgentOrgId"](`agent-${i}`);
}
// @ts-expect-error
expect(manager["agentOwnerCache"].size).toBeLessThanOrEqual(1024);
// @ts-expect-error
expect(manager["agentOrgCache"].size).toBeLessThanOrEqual(1024);
});
});
15 changes: 13 additions & 2 deletions packages/server/src/gateway/auth/settings/auth-profiles-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ export class AuthProfilesManager {
>();
private static readonly AGENT_OWNER_CACHE_TTL_MS = 60_000;
private static readonly AGENT_ORG_CACHE_TTL_MS = 60_000;
/** Hard cap on either cache to bound retention if many distinct agents are
* looked up but never re-queried. When set() crosses this, the oldest
* insertion is evicted (Maps iterate in insertion order). */
private static readonly AGENT_CACHE_MAX_ENTRIES = 1024;
private cacheSet<V>(cache: Map<string, V>, key: string, value: V): void {
if (cache.size >= AuthProfilesManager.AGENT_CACHE_MAX_ENTRIES && !cache.has(key)) {
const oldest = cache.keys().next().value;
if (oldest !== undefined) cache.delete(oldest);
}
cache.set(key, value);
}
private lazyRefreshHooks?: LazyRefreshHooks;

constructor(options: AuthProfilesManagerOptions) {
Expand Down Expand Up @@ -230,7 +241,7 @@ export class AuthProfilesManager {
);
return undefined;
}
this.agentOwnerCache.set(agentId, {
this.cacheSet(this.agentOwnerCache, agentId, {
ownerUserId,
expiresAt: Date.now() + AuthProfilesManager.AGENT_OWNER_CACHE_TTL_MS,
});
Expand All @@ -256,7 +267,7 @@ export class AuthProfilesManager {
);
return undefined;
}
this.agentOrgCache.set(agentId, {
this.cacheSet(this.agentOrgCache, agentId, {
organizationId,
expiresAt: Date.now() + AuthProfilesManager.AGENT_ORG_CACHE_TTL_MS,
});
Expand Down
Loading