fix(agent): reload credentials dynamically
This commit is contained in:
parent
3b09d8d44d
commit
24df8b02e7
3 changed files with 106 additions and 11 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import JSON5 from "json5";
|
||||
|
|
@ -90,6 +90,8 @@ export class CredentialManager {
|
|||
private coreConfig: CredentialsConfig | null = null;
|
||||
private skillsConfig: SkillsEnvConfig | null = null;
|
||||
private resolvedSkillsEnv: Record<string, string> | null = null;
|
||||
private coreMtimeMs: number | null = null;
|
||||
private skillsMtimeMs: number | null = null;
|
||||
|
||||
private isDisabled(): boolean {
|
||||
if (process.env.SMC_CREDENTIALS_DISABLE === "1") return true;
|
||||
|
|
@ -99,17 +101,32 @@ export class CredentialManager {
|
|||
private loadCore(): void {
|
||||
const path = getCredentialsPath();
|
||||
const disabled = this.isDisabled();
|
||||
let mtimeMs: number | null = null;
|
||||
|
||||
if (this.corePath === path && this.disabledState === disabled && this.coreConfig) {
|
||||
if (!disabled && existsSync(path)) {
|
||||
try {
|
||||
mtimeMs = statSync(path).mtimeMs;
|
||||
} catch {
|
||||
mtimeMs = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.corePath === path
|
||||
&& this.disabledState === disabled
|
||||
&& this.coreConfig
|
||||
&& this.coreMtimeMs === mtimeMs
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.corePath = path;
|
||||
this.disabledState = disabled;
|
||||
this.coreConfig = null;
|
||||
this.coreMtimeMs = mtimeMs;
|
||||
|
||||
if (disabled) return;
|
||||
if (!existsSync(path)) return;
|
||||
if (mtimeMs === null) return;
|
||||
|
||||
const raw = readFileSync(path, "utf8");
|
||||
try {
|
||||
|
|
@ -123,8 +140,22 @@ export class CredentialManager {
|
|||
private loadSkillsEnv(): void {
|
||||
const path = getSkillsEnvPath();
|
||||
const disabled = this.isDisabled();
|
||||
let mtimeMs: number | null = null;
|
||||
|
||||
if (this.skillsPath === path && this.disabledState === disabled && this.resolvedSkillsEnv) {
|
||||
if (!disabled && existsSync(path)) {
|
||||
try {
|
||||
mtimeMs = statSync(path).mtimeMs;
|
||||
} catch {
|
||||
mtimeMs = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.skillsPath === path
|
||||
&& this.disabledState === disabled
|
||||
&& this.resolvedSkillsEnv
|
||||
&& this.skillsMtimeMs === mtimeMs
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -132,9 +163,10 @@ export class CredentialManager {
|
|||
this.disabledState = disabled;
|
||||
this.skillsConfig = null;
|
||||
this.resolvedSkillsEnv = null;
|
||||
this.skillsMtimeMs = mtimeMs;
|
||||
|
||||
if (disabled) return;
|
||||
if (!existsSync(path)) return;
|
||||
if (mtimeMs === null) return;
|
||||
|
||||
const raw = readFileSync(path, "utf8");
|
||||
try {
|
||||
|
|
@ -228,6 +260,8 @@ export class CredentialManager {
|
|||
this.coreConfig = null;
|
||||
this.skillsConfig = null;
|
||||
this.resolvedSkillsEnv = null;
|
||||
this.coreMtimeMs = null;
|
||||
this.skillsMtimeMs = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ export function getLoginInstructions(providerId: string): string {
|
|||
|
||||
if (info.authMethod === "oauth") {
|
||||
if (info.loginCommand) {
|
||||
return `Run: ${info.loginCommand}\nThen restart Super Multica to use the credentials.`;
|
||||
return `Run: ${info.loginCommand}\nThen retry in Super Multica to use the credentials.`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export class Agent {
|
|||
private profileCandidates: string[];
|
||||
private profileIndex: number;
|
||||
private readonly pinnedProfile: boolean;
|
||||
private readonly explicitApiKey: boolean;
|
||||
|
||||
/** Current session ID */
|
||||
readonly sessionId: string;
|
||||
|
|
@ -126,6 +127,7 @@ export class Agent {
|
|||
|
||||
// === Auth profile resolution ===
|
||||
this.pinnedProfile = !!(options.authProfileId || options.apiKey);
|
||||
this.explicitApiKey = !!options.apiKey;
|
||||
|
||||
if (options.apiKey) {
|
||||
// Explicit API key — no rotation
|
||||
|
|
@ -159,11 +161,9 @@ export class Agent {
|
|||
: 0;
|
||||
}
|
||||
|
||||
this.agent = new PiAgentCore(
|
||||
this.currentApiKey
|
||||
? { getApiKey: (_provider: string) => this.currentApiKey! }
|
||||
: {},
|
||||
);
|
||||
this.agent = new PiAgentCore({
|
||||
getApiKey: (_provider: string) => this.currentApiKey ?? "",
|
||||
});
|
||||
|
||||
// Load Agent Profile (if profileId is specified)
|
||||
// Every Agent should have a Profile for memory, tools config, and other settings
|
||||
|
|
@ -354,6 +354,10 @@ export class Agent {
|
|||
|
||||
async run(prompt: string): Promise<AgentRunResult> {
|
||||
await this.ensureInitialized();
|
||||
this.refreshAuthState();
|
||||
if (!this.currentApiKey) {
|
||||
throw new Error(`No API key configured for provider: ${this.resolvedProvider}`);
|
||||
}
|
||||
this.output.state.lastAssistantText = "";
|
||||
|
||||
const canRotate = !this.pinnedProfile && this.profileCandidates.length > 1;
|
||||
|
|
@ -433,12 +437,67 @@ export class Agent {
|
|||
this.currentApiKey = apiKey;
|
||||
this.currentProfileId = candidateId;
|
||||
this.profileIndex = nextIndex;
|
||||
this.updateSessionApiKey();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private refreshAuthState(): void {
|
||||
if (this.explicitApiKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const store = loadAuthProfileStore();
|
||||
|
||||
if (this.pinnedProfile) {
|
||||
const profileId = this.currentProfileId ?? this.resolvedProvider;
|
||||
this.currentApiKey = resolveApiKeyForProfile(profileId) ?? resolveApiKey(this.resolvedProvider);
|
||||
this.currentProfileId = profileId;
|
||||
this.profileCandidates = [];
|
||||
this.profileIndex = 0;
|
||||
this.updateSessionApiKey();
|
||||
return;
|
||||
}
|
||||
|
||||
const candidates = resolveAuthProfileOrder(this.resolvedProvider, store);
|
||||
this.profileCandidates = candidates;
|
||||
|
||||
if (this.currentProfileId) {
|
||||
const currentIndex = candidates.indexOf(this.currentProfileId);
|
||||
if (currentIndex >= 0) {
|
||||
const stats = store.usageStats?.[this.currentProfileId];
|
||||
if (!stats || !isProfileInCooldown(stats)) {
|
||||
const apiKey = resolveApiKeyForProfile(this.currentProfileId);
|
||||
if (apiKey) {
|
||||
this.currentApiKey = apiKey;
|
||||
this.profileIndex = currentIndex;
|
||||
this.updateSessionApiKey();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolved = resolveApiKeyForProvider(this.resolvedProvider);
|
||||
if (resolved) {
|
||||
this.currentApiKey = resolved.apiKey;
|
||||
this.currentProfileId = resolved.profileId;
|
||||
this.profileIndex = Math.max(0, candidates.indexOf(resolved.profileId));
|
||||
} else {
|
||||
this.currentApiKey = undefined;
|
||||
this.currentProfileId = undefined;
|
||||
this.profileIndex = 0;
|
||||
}
|
||||
this.updateSessionApiKey();
|
||||
}
|
||||
|
||||
private updateSessionApiKey(): void {
|
||||
if (this.session.getCompactionMode() !== "summary") return;
|
||||
this.session.setApiKey(this.currentApiKey);
|
||||
}
|
||||
|
||||
private handleSessionEvent(event: AgentEvent) {
|
||||
if (event.type === "message_end") {
|
||||
const message = event.message as AgentMessage;
|
||||
|
|
@ -680,6 +739,8 @@ export class Agent {
|
|||
throw new Error(`No API key configured for provider: ${providerId}`);
|
||||
}
|
||||
|
||||
this.updateSessionApiKey();
|
||||
|
||||
// Update the agent's model and API key
|
||||
const baseUrl = resolveBaseUrl(actualProvider);
|
||||
const modelWithBaseUrl = baseUrl ? { ...model, baseUrl } : model;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue