feat(pool): pin a user to one Claude/Codex pool account
An admin pins a server-mode user to one pool account per tool (account_pins; PUT /api/accounts/pins with the password step-up and audit, the Users page's Pin a user, agenthub accounts pin|unpin). The workspace owner's pin wins over the balancer whenever the pane may use the account; a pinned login still waits while another pool login holds the home.
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
* the last 5 h, then 7 d. The reading is the newest `rate_limits` in the
|
||||
* rollouts of the sessions that account's runs started (no network), each
|
||||
* from the CODEX_HOME of the account its workspace runs as.
|
||||
* A user an admin pinned to one account (account-pins.js) skips the ranking.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -139,6 +140,7 @@ function removeAccount(name) {
|
||||
const n = validName(name);
|
||||
if (!readAccount(n)) throw err("ENOACCOUNT", `no account ${n} in the pool`);
|
||||
fs.rmSync(fileOf(n));
|
||||
require("./account-pins").dropAccount("codex", n);
|
||||
quotaCache = null;
|
||||
}
|
||||
|
||||
@@ -332,10 +334,15 @@ function syncBack() {
|
||||
|
||||
// ── Picking ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** The full record a new Codex pane should run on, or null. `member` = run-as account. */
|
||||
function pick(member) {
|
||||
/**
|
||||
* The full record a new Codex pane should run on, or null. `member` = run-as
|
||||
* account; `ownerId` = the workspace owner, whose pin (account-pins.js) wins
|
||||
* over the ranking when this pane may use that account.
|
||||
*/
|
||||
function pick(member, ownerId = null) {
|
||||
let usable = allAccounts();
|
||||
if (usable.length === 0) return null;
|
||||
let live = null;
|
||||
if (!member) {
|
||||
// A host pane's CODEX_HOME holds the host's own login.
|
||||
usable = usable.filter((a) => a.kind === "apikey");
|
||||
@@ -348,10 +355,15 @@ function pick(member) {
|
||||
} else {
|
||||
// One auth.json per home: while a pane of theirs runs on a pool
|
||||
// login, the next pane stays on that one.
|
||||
const live = usable.find((a) => a.kind === "chatgpt" && liveInHome(a.name, member));
|
||||
if (live) return live;
|
||||
live = usable.find((a) => a.kind === "chatgpt" && liveInHome(a.name, member)) || null;
|
||||
}
|
||||
}
|
||||
// A pinned API key goes in by env and wins outright; a pinned ChatGPT login
|
||||
// waits while another login holds this home.
|
||||
const pin = require("./account-pins").pinFor(ownerId, "codex");
|
||||
const pinned = pin ? usable.find((a) => a.name === pin) : null;
|
||||
if (pinned && (pinned.kind === "apikey" || !live || live === pinned)) return pinned;
|
||||
if (live) return live;
|
||||
if (usable.length === 0) return null;
|
||||
const ranked = usable
|
||||
.map((a) => {
|
||||
@@ -395,8 +407,8 @@ function liveInHome(name, member) {
|
||||
}
|
||||
|
||||
/** `{name, kind}` of the account a new Codex pane would get, or null. */
|
||||
function pickAccount(member = null) {
|
||||
const a = pick(member);
|
||||
function pickAccount(member = null, ownerId = null) {
|
||||
const a = pick(member, ownerId);
|
||||
return a ? { name: a.name, kind: a.kind } : null;
|
||||
}
|
||||
|
||||
@@ -405,8 +417,8 @@ function pickAccount(member = null) {
|
||||
* this pane may use): `{name, argv, env, place}`. `env` goes through tmux `-e`
|
||||
* (a member's launch file); `place()` runs just before the pane starts.
|
||||
*/
|
||||
function forSpawn(member = null) {
|
||||
const a = pick(member);
|
||||
function forSpawn(member = null, ownerId = null) {
|
||||
const a = pick(member, ownerId);
|
||||
if (!a) return null;
|
||||
if (a.kind === "apikey") {
|
||||
const provider = `{name="OpenAI (AgentHub pool)",base_url="https://api.openai.com/v1",env_key="${KEY_ENV}",wire_api="responses"}`;
|
||||
@@ -439,8 +451,12 @@ function forSpawn(member = null) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Is `name` an account in the pool? */
|
||||
const hasAccount = (name) => NAME_RE.test(String(name)) && Boolean(readAccount(String(name)));
|
||||
|
||||
module.exports = {
|
||||
BLOCK_PERCENT,
|
||||
hasAccount,
|
||||
addAccount,
|
||||
removeAccount,
|
||||
poolStatus,
|
||||
|
||||
Reference in New Issue
Block a user