fix: merge subagent auth profiles
This commit is contained in:
@@ -51,8 +51,8 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- WhatsApp: default response prefix only for self-chat, using identity name when set.
|
- WhatsApp: default response prefix only for self-chat, using identity name when set.
|
||||||
- Auth: merge main auth profiles into per-agent stores for sub-agents and document inheritance. (#1013) — thanks @marcmarg.
|
- Auth: merge main auth profiles into per-agent stores for sub-agents and document inheritance. (#1013) — thanks @marcmarg.
|
||||||
- Agents: avoid JSON Schema `format` collisions in tool params by renaming snapshot format fields. (#1013) — thanks @marcmarg.
|
- Agents: avoid JSON Schema `format` collisions in tool params by renaming snapshot format fields. (#1013) — thanks @marcmarg.
|
||||||
- Fix: make `clawdbot update` auto-update global installs when installed via a package manager.
|
- Fix: make `clawdbot update` auto-update global installs when installed via a package manager.
|
||||||
- Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
|
- Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
|
||||||
- Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr.
|
- Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr.
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ Auto-archive:
|
|||||||
- Auto-archive is best-effort; pending timers are lost if the gateway restarts.
|
- Auto-archive is best-effort; pending timers are lost if the gateway restarts.
|
||||||
- `runTimeoutSeconds` does **not** auto-archive; it only stops the run. The session remains until auto-archive.
|
- `runTimeoutSeconds` does **not** auto-archive; it only stops the run. The session remains until auto-archive.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Sub-agent auth is resolved by **agent id**, not by session type:
|
||||||
|
- The sub-agent session key is `agent:<agentId>:subagent:<uuid>`.
|
||||||
|
- The auth store is loaded from that agent’s `agentDir`.
|
||||||
|
- The main agent’s auth profiles are merged in as a **fallback**; agent profiles override main profiles on conflicts.
|
||||||
|
|
||||||
|
Note: the merge is additive, so main profiles are always available as fallbacks. Fully isolated auth per agent is not supported yet.
|
||||||
|
|
||||||
## Announce
|
## Announce
|
||||||
|
|
||||||
Sub-agents report back via an announce step:
|
Sub-agents report back via an announce step:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||||
|
import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js";
|
||||||
|
|
||||||
describe("ensureAuthProfileStore", () => {
|
describe("ensureAuthProfileStore", () => {
|
||||||
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
||||||
@@ -45,4 +46,80 @@ describe("ensureAuthProfileStore", () => {
|
|||||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("merges main auth profiles into agent store and keeps agent overrides", () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-auth-merge-"));
|
||||||
|
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
|
||||||
|
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||||
|
try {
|
||||||
|
const mainDir = path.join(root, "main-agent");
|
||||||
|
const agentDir = path.join(root, "agent-x");
|
||||||
|
fs.mkdirSync(mainDir, { recursive: true });
|
||||||
|
fs.mkdirSync(agentDir, { recursive: true });
|
||||||
|
|
||||||
|
process.env.CLAWDBOT_AGENT_DIR = mainDir;
|
||||||
|
process.env.PI_CODING_AGENT_DIR = mainDir;
|
||||||
|
|
||||||
|
const mainStore = {
|
||||||
|
version: AUTH_STORE_VERSION,
|
||||||
|
profiles: {
|
||||||
|
"openai:default": {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "openai",
|
||||||
|
key: "main-key",
|
||||||
|
},
|
||||||
|
"anthropic:default": {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "anthropic",
|
||||||
|
key: "main-anthropic-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(mainDir, "auth-profiles.json"),
|
||||||
|
`${JSON.stringify(mainStore, null, 2)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const agentStore = {
|
||||||
|
version: AUTH_STORE_VERSION,
|
||||||
|
profiles: {
|
||||||
|
"openai:default": {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "openai",
|
||||||
|
key: "agent-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(agentDir, "auth-profiles.json"),
|
||||||
|
`${JSON.stringify(agentStore, null, 2)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
expect(store.profiles["anthropic:default"]).toMatchObject({
|
||||||
|
type: "api_key",
|
||||||
|
provider: "anthropic",
|
||||||
|
key: "main-anthropic-key",
|
||||||
|
});
|
||||||
|
expect(store.profiles["openai:default"]).toMatchObject({
|
||||||
|
type: "api_key",
|
||||||
|
provider: "openai",
|
||||||
|
key: "agent-key",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (previousAgentDir === undefined) {
|
||||||
|
delete process.env.CLAWDBOT_AGENT_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
|
||||||
|
}
|
||||||
|
if (previousPiAgentDir === undefined) {
|
||||||
|
delete process.env.PI_CODING_AGENT_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||||
|
}
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,6 +111,34 @@ function coerceAuthStore(raw: unknown): AuthProfileStore | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeRecord<T>(
|
||||||
|
base?: Record<string, T>,
|
||||||
|
override?: Record<string, T>,
|
||||||
|
): Record<string, T> | undefined {
|
||||||
|
if (!base && !override) return undefined;
|
||||||
|
if (!base) return { ...override };
|
||||||
|
if (!override) return { ...base };
|
||||||
|
return { ...base, ...override };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAuthProfileStores(base: AuthProfileStore, override: AuthProfileStore): AuthProfileStore {
|
||||||
|
if (
|
||||||
|
Object.keys(override.profiles).length === 0 &&
|
||||||
|
!override.order &&
|
||||||
|
!override.lastGood &&
|
||||||
|
!override.usageStats
|
||||||
|
) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: Math.max(base.version, override.version ?? base.version),
|
||||||
|
profiles: { ...base.profiles, ...override.profiles },
|
||||||
|
order: mergeRecord(base.order, override.order),
|
||||||
|
lastGood: mergeRecord(base.lastGood, override.lastGood),
|
||||||
|
usageStats: mergeRecord(base.usageStats, override.usageStats),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
|
function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
|
||||||
const oauthPath = resolveOAuthPath();
|
const oauthPath = resolveOAuthPath();
|
||||||
const oauthRaw = loadJsonFile(oauthPath);
|
const oauthRaw = loadJsonFile(oauthPath);
|
||||||
@@ -191,7 +219,7 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureAuthProfileStore(
|
function loadAuthProfileStoreForAgent(
|
||||||
agentDir?: string,
|
agentDir?: string,
|
||||||
options?: { allowKeychainPrompt?: boolean },
|
options?: { allowKeychainPrompt?: boolean },
|
||||||
): AuthProfileStore {
|
): AuthProfileStore {
|
||||||
@@ -207,6 +235,19 @@ export function ensureAuthProfileStore(
|
|||||||
return asStore;
|
return asStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: inherit auth-profiles from main agent if subagent has none
|
||||||
|
if (agentDir) {
|
||||||
|
const mainAuthPath = resolveAuthStorePath(); // without agentDir = main
|
||||||
|
const mainRaw = loadJsonFile(mainAuthPath);
|
||||||
|
const mainStore = coerceAuthStore(mainRaw);
|
||||||
|
if (mainStore && Object.keys(mainStore.profiles).length > 0) {
|
||||||
|
// Clone main store to subagent directory for auth inheritance
|
||||||
|
saveJsonFile(authPath, mainStore);
|
||||||
|
log.info("inherited auth-profiles from main agent", { agentDir });
|
||||||
|
return mainStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath(agentDir));
|
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath(agentDir));
|
||||||
const legacy = coerceLegacyStore(legacyRaw);
|
const legacy = coerceLegacyStore(legacyRaw);
|
||||||
const store: AuthProfileStore = {
|
const store: AuthProfileStore = {
|
||||||
@@ -274,6 +315,21 @@ export function ensureAuthProfileStore(
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ensureAuthProfileStore(
|
||||||
|
agentDir?: string,
|
||||||
|
options?: { allowKeychainPrompt?: boolean },
|
||||||
|
): AuthProfileStore {
|
||||||
|
const store = loadAuthProfileStoreForAgent(agentDir, options);
|
||||||
|
const authPath = resolveAuthStorePath(agentDir);
|
||||||
|
const mainAuthPath = resolveAuthStorePath();
|
||||||
|
if (!agentDir || authPath === mainAuthPath) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||||
|
return mergeAuthProfileStores(mainStore, store);
|
||||||
|
}
|
||||||
|
|
||||||
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
|
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
|
||||||
const authPath = resolveAuthStorePath(agentDir);
|
const authPath = resolveAuthStorePath(agentDir);
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
Reference in New Issue
Block a user