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:
@@ -16,6 +16,9 @@
|
||||
* refreshes); `credentials` — the text of a `.credentials.json` (or a macOS
|
||||
* Keychain item's JSON); or `token` — a `claude setup-token` token. A Codex
|
||||
* one is an OpenAI API key or the text of an `auth.json` (`token`).
|
||||
* `PUT /pins` (`{tool, userId, account}`, `account: null` unpins) pins one
|
||||
* user's new sessions of a tool to one pool account (account-pins.js); `GET /`
|
||||
* lists the pins under `pins`.
|
||||
* `GET /profiles` lists the saved profiles that can be moved in (identity only). Like every Users-page change
|
||||
* they need the admin's own password again (`currentPassword`), are audited
|
||||
* (the account NAME only), and refuse in local mode. The secret only goes IN:
|
||||
@@ -29,6 +32,7 @@ const codexPool = require("../lib/codex-pool");
|
||||
const auth = require("../lib/auth");
|
||||
const { recordAccountChange } = require("../lib/audit");
|
||||
const profiles = require("../lib/cc-profiles");
|
||||
const pins = require("../lib/account-pins");
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -39,6 +43,9 @@ router.get("/", async (req, res) => {
|
||||
res.json({
|
||||
accounts: pool.poolStatus(),
|
||||
codex: codexPool.poolStatus(),
|
||||
pins: auth.isServerMode()
|
||||
? { claude: pins.listPins("claude"), codex: pins.listPins("codex") }
|
||||
: { claude: [], codex: [] },
|
||||
blockPercent: pool.BLOCK_PERCENT,
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -72,6 +79,7 @@ const STATUS = {
|
||||
EBADNAME: 400,
|
||||
EBADTOKEN: 400,
|
||||
ENOACCOUNT: 404,
|
||||
ENOUSER: 404,
|
||||
EEXISTS: 409,
|
||||
ETOOMANY: 429,
|
||||
ESTALE: 409, // a pasted login whose refresh token was already spent
|
||||
@@ -93,7 +101,7 @@ function poolChange(handler) {
|
||||
}
|
||||
await auth.confirmPassword(req.user, body.currentPassword);
|
||||
const out = await handler(req, body);
|
||||
recordAccountChange(req.user.id, null, req.method, out.action);
|
||||
recordAccountChange(req.user.id, out.target ?? null, req.method, out.action);
|
||||
res.status(out.status || 200).json(out.json);
|
||||
} catch (e) {
|
||||
const status = STATUS[e.code];
|
||||
@@ -203,6 +211,20 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/pins",
|
||||
poolChange((_req, body) => {
|
||||
const pin = pins.setPin(Number(body.userId), body.tool, body.account ?? null);
|
||||
return {
|
||||
json: { pin },
|
||||
target: pin.userId,
|
||||
action: pin.account
|
||||
? `pool: pin ${pin.username} to ${body.tool} account ${pin.account}`
|
||||
: `pool: unpin ${pin.username} from ${body.tool}`,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:name",
|
||||
poolChange((req, body) => {
|
||||
|
||||
Reference in New Issue
Block a user