mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
Codex round 4: four P2s, every one an interleaving of the same window — escalation as reclaim-then-repost is a compound, non-atomic operation whose continuation must revalidate the world (FIFO position lost, ref assigned too late, no run fence, competing bubble actions). Rounds 1-3 patched that window with a lock and rechecks; round 4 shows the window itself is the defect, so this removes it instead of guarding it again. Escalation is now POST /chat/steer/arm: the server flips preempt on the EXISTING queued item in one atomic store op (new IJobStore.armSteer; a decode-patch-encode LSET Lua on Redis, an in-place mutation in memory), fenced to the validated generation and refused once the queue closes. The handler mirrors the steer POST's preempt contract exactly: durable flag gated on the owner's recorded capability, volatile requestPreempt fire-and-forget because the durable flag is the truth resume/handover re-arm from. By construction this resolves all four findings: FIFO survives (the item never moves; the whole queue still drains in instruction order at the seal), there is no continuation to hold stale controls, the store op is fenced to the original run, and a competing Edit/Queue/Cancel either beats the arm (armed:false, chip untouched) or operates on the armed item, whose cancel already disarms. The client escalation entry becomes one mutation: armed:true relabels the chip in place (same steerId, same position), PREEMPT_UNSUPPORTED and lost races toast honestly, and the round 1-3 machinery — the escalating lock atom, the latest-ref, the post-reclaim rechecks and their two toast strings — is deleted rather than extended. Verified: 7 new handler tests on the real in-memory manager (including FIFO preservation and the stale-generation fence), 2 Redis integration tests against real Redis (in-place arm keeps order and every field; missing/stale/closed all refuse), client suites 396 green.
136 lines
4.7 KiB
JavaScript
136 lines
4.7 KiB
JavaScript
const {
|
|
checkAccess,
|
|
handleSteerRequest,
|
|
handleSteerCancel,
|
|
handleSteerArm,
|
|
} = require('@librechat/api');
|
|
const { logger, ResourceCapabilityMap } = require('@librechat/data-schemas');
|
|
const {
|
|
Permissions,
|
|
ResourceType,
|
|
PermissionBits,
|
|
PermissionTypes,
|
|
isAgentsEndpoint,
|
|
isEphemeralAgentId,
|
|
} = require('librechat-data-provider');
|
|
const { checkPermission } = require('~/server/services/PermissionService');
|
|
const { hasCapability } = require('~/server/middleware/roles/capabilities');
|
|
const db = require('~/models');
|
|
|
|
/**
|
|
* Steer-time agent authorization, mirroring the chat route's middlewares
|
|
* (`checkAgentAccess` + `canAccessAgentFromBody`) against the ORIGINATING
|
|
* run's identity from job metadata instead of the request body:
|
|
* - role gate: AGENTS:USE via `checkAccess`, applied exactly when chat.js
|
|
* would run it (`skipAgentCheck` skips non-agents endpoints);
|
|
* - resource gate: `canAccessResource`'s capability bypass + `checkPermission`
|
|
* VIEW on the resolved agent, skipped for ephemeral/no-agent runs.
|
|
*
|
|
* @param {import('express').Request} req
|
|
* @returns {(run: import('@librechat/api').SteerRunContext) => Promise<boolean>}
|
|
*/
|
|
const createAgentAccessCheck =
|
|
(req) =>
|
|
async ({ agentId, endpoint }) => {
|
|
const hasRealAgent = agentId != null && !isEphemeralAgentId(agentId);
|
|
const roleGateApplies = endpoint == null ? hasRealAgent : isAgentsEndpoint(endpoint);
|
|
if (roleGateApplies) {
|
|
const roleAllowed = await checkAccess({
|
|
req,
|
|
user: req.user,
|
|
permissionType: PermissionTypes.AGENTS,
|
|
permissions: [Permissions.USE],
|
|
getRoleByName: db.getRoleByName,
|
|
});
|
|
if (!roleAllowed) {
|
|
return false;
|
|
}
|
|
}
|
|
if (!hasRealAgent) {
|
|
return true;
|
|
}
|
|
let bypass = false;
|
|
try {
|
|
bypass = await hasCapability(req.user, ResourceCapabilityMap[ResourceType.AGENT]);
|
|
} catch {
|
|
bypass = false;
|
|
}
|
|
if (bypass) {
|
|
return true;
|
|
}
|
|
const agent = await db.getAgent({ id: agentId });
|
|
if (!agent) {
|
|
return false;
|
|
}
|
|
return checkPermission({
|
|
userId: req.user.id,
|
|
role: req.user.role,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
requiredPermission: PermissionBits.VIEW,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* POST /api/agents/chat/steer
|
|
*
|
|
* Thin wrapper: the full guard ladder (validation, file sanitization,
|
|
* capability gate, ownership/tenant checks, agent access, owner-scoped file
|
|
* resolve, status-guarded enqueue) lives in `@librechat/api`
|
|
* (`handleSteerRequest`), which returns the HTTP status + JSON body to
|
|
* serialize verbatim. DB access and permission services are injected here.
|
|
*/
|
|
const SteerController = async (req, res) => {
|
|
try {
|
|
const { status, body } = await handleSteerRequest(req.user ?? {}, req.body ?? {}, {
|
|
getFiles: db.getFiles,
|
|
updateFilesUsage: db.updateFilesUsage,
|
|
checkAgentAccess: createAgentAccessCheck(req),
|
|
});
|
|
return res.status(status).json(body);
|
|
} catch (error) {
|
|
logger.error('[SteerController] Failed to queue steer', error);
|
|
return res.status(500).json({ code: 'STEER_FAILED' });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* POST /api/agents/chat/steer/cancel
|
|
*
|
|
* Removes a still-queued steer before injection. `removed: false` is not an
|
|
* error — the cancel lost its race (already injected, or the run ended) and
|
|
* the client defers to the events it will receive. No agent-access check:
|
|
* a cancel injects nothing model-bound, so ownership checks suffice.
|
|
*/
|
|
const SteerCancelController = async (req, res) => {
|
|
try {
|
|
const { status, body } = await handleSteerCancel(req.user ?? {}, req.body ?? {});
|
|
return res.status(status).json(body);
|
|
} catch (error) {
|
|
logger.error('[SteerCancelController] Failed to cancel steer', error);
|
|
return res.status(500).json({ code: 'STEER_CANCEL_FAILED' });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* POST /api/agents/chat/steer/arm
|
|
*
|
|
* Escalates a still-queued steer to an interrupt in place (the durable item
|
|
* keeps its FIFO position). `armed: false` is not an error — the steer
|
|
* already injected, was cancelled, or the deployment cannot seal mid-stream.
|
|
* No agent-access check: arming injects nothing model-bound, so ownership
|
|
* checks suffice, exactly like cancel.
|
|
*/
|
|
const SteerArmController = async (req, res) => {
|
|
try {
|
|
const { status, body } = await handleSteerArm(req.user ?? {}, req.body ?? {});
|
|
return res.status(status).json(body);
|
|
} catch (error) {
|
|
logger.error('[SteerArmController] Failed to arm steer', error);
|
|
return res.status(500).json({ code: 'STEER_ARM_FAILED' });
|
|
}
|
|
};
|
|
|
|
module.exports = SteerController;
|
|
module.exports.SteerCancelController = SteerCancelController;
|
|
module.exports.SteerArmController = SteerArmController;
|