feat(settings): every AI tool's login profiles in one Settings section, linked to the account pool
Settings → AI tools → Profiles holds one tab per tool: Claude Code (the cc-profiles panel, moved out of the Claude Code page's modal — its button now links here), Codex (the whole $CODEX_HOME/auth.json) and Oh My Pi (`omp auth export` / `omp auth import -`, secrets on stdin; omp's credential tables are emptied first because import merges, and a failed import puts the outgoing set back). Antigravity is listed as not switchable: agy keeps its login in the OS keyring. server/lib/tool-profiles.js + /api/tool-profiles keep cc-profiles' rules (one login = one profile, the outgoing login is re-saved before a switch, an unsaved one refuses it, the live login's profile is not deletable) and never return a token; /tool-profiles joins ADMIN_WRITE_PREFIXES and STAFF_ONLY_PREFIXES, writes are same-origin only. The account pool takes a saved Codex profile too (moved in like a Claude one, GET /api/accounts/profiles?tool=codex). Each Claude/Codex profile has a "Move to pool" link (/admin?poolAdd=&poolProfile= opens the pool's Add dialog on it), Settings shows the pool's size, and the pool card links back.
This commit is contained in:
@@ -338,6 +338,42 @@ describe("moving a saved profile in from the Users page", () => {
|
||||
pool.removeAccount("work");
|
||||
});
|
||||
|
||||
it("moves a saved Codex profile in, and lists Codex profiles without a token", async () => {
|
||||
const dir = path.join(TMP, ".claude", "agenthub-profiles", "codex");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const idToken = `x.${Buffer.from(JSON.stringify({ email: "cx@example.com" })).toString("base64url")}.s`;
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "cx.json"),
|
||||
JSON.stringify({
|
||||
name: "cx",
|
||||
label: "Codex work",
|
||||
secret: {
|
||||
tokens: {
|
||||
id_token: idToken,
|
||||
access_token: "at-cx",
|
||||
refresh_token: "rt-cx",
|
||||
account_id: "a-cx",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const list = await call("GET", "/api/accounts/profiles?tool=codex");
|
||||
assert.deepEqual(
|
||||
list.body.profiles.map((p) => [p.name, p.label, p.email]),
|
||||
[["cx", "Codex work", "cx@example.com"]]
|
||||
);
|
||||
assert.ok(!JSON.stringify(list.body).includes("rt-cx"));
|
||||
const moved = await call("POST", "/api/accounts", {
|
||||
tool: "codex",
|
||||
profile: "cx",
|
||||
currentPassword: "admin-pass-1",
|
||||
});
|
||||
assert.equal(moved.status, 201);
|
||||
assert.equal(moved.body.account.kind, "chatgpt");
|
||||
assert.equal(fs.existsSync(path.join(dir, "cx.json")), false, "the pool is its one holder");
|
||||
require("../lib/codex-pool").removeAccount("cx");
|
||||
});
|
||||
|
||||
it("takes a pasted .credentials.json once Anthropic names its account", async () => {
|
||||
const pasted = oauth("p");
|
||||
owners[pasted.accessToken] = { uuid: "uuid-pasted", email: "p@example.com" };
|
||||
|
||||
Reference in New Issue
Block a user