Merge ea1d5a30dbebd4fa59e1d443aa138230c1b0a448 into 598f1826d8b2bc969aace2c6459824737667218c
This commit is contained in:
commit
04b5c33f52
@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AgentEntrySchema } from "../../config/zod-schema.agent-runtime.js";
|
||||
import { resolveAuthProfileOrder } from "./order.js";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
|
||||
@ -22,4 +23,130 @@ describe("resolveAuthProfileOrder", () => {
|
||||
|
||||
expect(order).toEqual(["volcengine:default"]);
|
||||
});
|
||||
|
||||
it("places per-agent preferredProfile first in the order", () => {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:account1": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-1",
|
||||
},
|
||||
"openai:account2": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-2",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const order = resolveAuthProfileOrder({
|
||||
store,
|
||||
provider: "openai",
|
||||
preferredProfile: "openai:account2",
|
||||
});
|
||||
|
||||
// The preferred profile should be first in the ordering
|
||||
expect(order[0]).toBe("openai:account2");
|
||||
expect(order).toContain("openai:account1");
|
||||
});
|
||||
|
||||
it("ignores preferredProfile that does not exist in store", () => {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:account1": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-1",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const order = resolveAuthProfileOrder({
|
||||
store,
|
||||
provider: "openai",
|
||||
preferredProfile: "openai:nonexistent",
|
||||
});
|
||||
|
||||
expect(order).toEqual(["openai:account1"]);
|
||||
});
|
||||
|
||||
it("per-agent binding overrides session-level profile via effectivePreferredProfile", () => {
|
||||
// Simulates the integration scenario where a per-agent binding (account2)
|
||||
// should take priority over a session-level preferred profile (account1).
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:account1": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-1",
|
||||
},
|
||||
"openai:account2": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-2",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Session requests account1, but per-agent config binds to account2.
|
||||
const sessionPreferred = "openai:account1";
|
||||
const agentAuthBinding = "openai:account2";
|
||||
const effectivePreferred = agentAuthBinding || sessionPreferred;
|
||||
|
||||
const order = resolveAuthProfileOrder({
|
||||
store,
|
||||
provider: "openai",
|
||||
preferredProfile: effectivePreferred,
|
||||
});
|
||||
|
||||
// Per-agent binding (account2) must come first, not the session override (account1)
|
||||
expect(order[0]).toBe("openai:account2");
|
||||
expect(order).toContain("openai:account1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentEntrySchema auth field", () => {
|
||||
it("accepts an agent entry with per-agent auth bindings", () => {
|
||||
const result = AgentEntrySchema.safeParse({
|
||||
id: "my-agent",
|
||||
auth: {
|
||||
openai: "openai:account2",
|
||||
anthropic: "anthropic:team-key",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.auth).toEqual({
|
||||
openai: "openai:account2",
|
||||
anthropic: "anthropic:team-key",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts an agent entry without auth field (backward compat)", () => {
|
||||
const result = AgentEntrySchema.safeParse({
|
||||
id: "my-agent",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.auth).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects auth field with non-string values", () => {
|
||||
const result = AgentEntrySchema.safeParse({
|
||||
id: "my-agent",
|
||||
auth: {
|
||||
openai: 123,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -423,7 +423,19 @@ export async function runEmbeddedPiAgent(
|
||||
|
||||
const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
|
||||
const preferredProfileId = params.authProfileId?.trim();
|
||||
let lockedProfileId = params.authProfileIdSource === "user" ? preferredProfileId : undefined;
|
||||
|
||||
// Per-agent auth binding: if config specifies a dedicated profile for this
|
||||
// agent+provider combination, use it as the preferred profile. This gives
|
||||
// per-agent bindings priority over session-level overrides while
|
||||
// maintaining full backward compatibility for agents without bindings.
|
||||
const normalizedProvider = normalizeProviderId(provider);
|
||||
const agentAuthBinding = params.config?.agents?.list?.find(
|
||||
(a: { id: string }) => a.id === params.agentId,
|
||||
)?.auth?.[normalizedProvider];
|
||||
const effectivePreferredProfile = agentAuthBinding?.trim() || preferredProfileId;
|
||||
|
||||
let lockedProfileId =
|
||||
params.authProfileIdSource === "user" ? effectivePreferredProfile : undefined;
|
||||
if (lockedProfileId) {
|
||||
const lockedProfile = authStore.profiles[lockedProfileId];
|
||||
if (
|
||||
@ -437,7 +449,7 @@ export async function runEmbeddedPiAgent(
|
||||
cfg: params.config,
|
||||
store: authStore,
|
||||
provider,
|
||||
preferredProfile: preferredProfileId,
|
||||
preferredProfile: effectivePreferredProfile,
|
||||
});
|
||||
if (lockedProfileId && !profileOrder.includes(lockedProfileId)) {
|
||||
throw new Error(`Auth profile "${lockedProfileId}" is not configured for ${provider}.`);
|
||||
|
||||
@ -269,6 +269,27 @@ export async function noteAuthProfileHealth(params: {
|
||||
const store = ensureAuthProfileStore(undefined, {
|
||||
allowKeychainPrompt: params.allowKeychainPrompt,
|
||||
});
|
||||
|
||||
// Validate per-agent auth bindings reference existing profiles.
|
||||
const agents = params.cfg.agents?.list ?? [];
|
||||
const perAgentWarnings: string[] = [];
|
||||
for (const agent of agents) {
|
||||
if (agent.auth && typeof agent.auth === "object") {
|
||||
for (const [provider, profileId] of Object.entries(agent.auth)) {
|
||||
if (typeof profileId !== "string" || !profileId.trim()) {
|
||||
continue;
|
||||
}
|
||||
if (!store.profiles[profileId]) {
|
||||
perAgentWarnings.push(
|
||||
`- agent "${agent.id}": auth profile "${profileId}" (provider: ${provider}) not found in auth store`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (perAgentWarnings.length > 0) {
|
||||
note(perAgentWarnings.join("\n"), "Per-agent auth binding warnings");
|
||||
}
|
||||
const unusable = (() => {
|
||||
const now = Date.now();
|
||||
const out: string[] = [];
|
||||
|
||||
@ -204,6 +204,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Idle runtime TTL in minutes for ACP session workers before eligible cleanup.",
|
||||
"acp.runtime.installCommand":
|
||||
"Optional operator install/setup command shown by `/acp install` and `/acp doctor` when ACP backend wiring is missing.",
|
||||
"agents.list[].auth":
|
||||
'Per-agent auth profile bindings. Maps provider names to specific auth profile IDs (e.g. {"openai": "openai:account2"}) for rate limit isolation and per-agent billing.',
|
||||
"agents.list.*.skills":
|
||||
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
|
||||
"agents.list[].skills":
|
||||
|
||||
@ -56,6 +56,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
|
||||
"agents.list.*.identity.avatar": "Identity Avatar",
|
||||
"agents.list.*.skills": "Agent Skill Filter",
|
||||
"agents.list[].auth": "Per-Agent Auth",
|
||||
"agents.list[].runtime": "Agent Runtime",
|
||||
"agents.list[].runtime.type": "Agent Runtime Type",
|
||||
"agents.list[].runtime.acp": "Agent ACP Runtime",
|
||||
|
||||
@ -64,6 +64,8 @@ export type AgentConfig = {
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
/** Per-agent auth profile bindings. Maps provider names to specific auth profile IDs. */
|
||||
auth?: Record<string, string>;
|
||||
model?: AgentModelConfig;
|
||||
/** Optional allowlist of skills for this agent (omit = all skills; empty = none). */
|
||||
skills?: string[];
|
||||
|
||||
@ -766,6 +766,7 @@ export const AgentEntrySchema = z
|
||||
name: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
agentDir: z.string().optional(),
|
||||
auth: z.record(z.string(), z.string()).optional(),
|
||||
model: AgentModelSchema.optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
memorySearch: MemorySearchSchema,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user