FEAT - LAUNCHER: модульный доступ Codex Agent API

This commit is contained in:
DCCONSTRUCTIONS
2026-05-14 20:32:26 +03:00
parent 5e18a7f3c2
commit 34917e007a
8 changed files with 463 additions and 15 deletions
+94
View File
@@ -11,6 +11,7 @@ const collectionKeys = [
"services",
"grants",
"exceptions",
"serviceModuleEntitlements",
"invites",
"accessRequests",
"revokedAccounts",
@@ -31,6 +32,7 @@ const grantStatuses = new Set(["active", "disabled"]);
const exceptionTypes = new Set(["deny", "allow"]);
const serviceStatuses = new Set(["active", "maintenance", "hidden", "disabled"]);
const taskManagerWorkspaceManagedByValues = new Set(["launcher", "tasker"]);
const serviceModuleIds = new Set(["codex_agents"]);
const accessRequestStatuses = new Set(["new", "approved", "rejected"]);
const taskerInviteRequestStatuses = new Set(["new", "approved", "rejected", "cancelled"]);
const taskManagerInviteRoles = new Set(["guest", "member", "admin"]);
@@ -345,6 +347,7 @@ export function createControlPlaneStore({ projectRoot }) {
!(grant.targetType === "client" && grant.targetId === clientId) &&
!(grant.targetType === "group" && deletedGroupIds.has(grant.targetId))
);
data.serviceModuleEntitlements = data.serviceModuleEntitlements.filter((entitlement) => entitlement.clientId !== clientId);
data.invites = data.invites.filter((invite) => invite.clientId !== clientId);
data.syncStatuses = data.syncStatuses.filter((syncStatus) => syncStatus.objectId !== clientId);
@@ -599,6 +602,7 @@ export function createControlPlaneStore({ projectRoot }) {
}));
data.grants = data.grants.filter((grant) => !(grant.targetType === "user" && grant.targetId === user.id));
data.exceptions = data.exceptions.filter((exception) => exception.userId !== user.id);
data.serviceModuleEntitlements = data.serviceModuleEntitlements.filter((entitlement) => entitlement.userId !== user.id);
data.invites = data.invites.filter(
(invite) => invite.email.toLowerCase() !== email && invite.invitedByUserId !== user.id
);
@@ -1551,6 +1555,72 @@ export function createControlPlaneStore({ projectRoot }) {
return { data };
}
async function setServiceModuleEntitlement(payload, identity) {
const data = readData();
const actor = resolveActor(data, identity);
const now = isoNow();
const clientId = requireString(payload?.clientId, "clientId");
const userId = requireString(payload?.userId, "userId");
const serviceId = requireString(payload?.serviceId, "serviceId");
const moduleId = pickEnum(payload?.moduleId, serviceModuleIds, "codex_agents");
const enabled = payload?.enabled === true;
const client = findById(data.clients, clientId, "client");
const user = findById(data.users, userId, "user");
const service = findById(data.services, serviceId, "service");
const existingEntitlement = data.serviceModuleEntitlements.find(
(entitlement) =>
entitlement.clientId === client.id &&
entitlement.userId === user.id &&
entitlement.serviceId === service.id &&
entitlement.moduleId === moduleId
);
let entitlement = null;
if (enabled) {
entitlement =
existingEntitlement ??
{
id: uniqueId(data.serviceModuleEntitlements, "svc_module", `${client.id}-${service.slug}-${user.email}-${moduleId}`),
clientId: client.id,
userId: user.id,
serviceId: service.id,
moduleId,
createdByUserId: actor.id,
createdAt: now,
};
entitlement.enabled = true;
entitlement.updatedAt = now;
if (!existingEntitlement) {
data.serviceModuleEntitlements.push(entitlement);
}
} else {
data.serviceModuleEntitlements = data.serviceModuleEntitlements.filter(
(candidate) =>
!(
candidate.clientId === client.id &&
candidate.userId === user.id &&
candidate.serviceId === service.id &&
candidate.moduleId === moduleId
)
);
}
addAuditEvent(data, actor, {
action: enabled ? "Включён модуль сервиса" : "Отключён модуль сервиса",
objectType: "service-module-entitlement",
objectName: `${service.slug}:${moduleId}:${user.email}`,
clientId: client.id,
result: "success",
details: `Module: ${moduleId}; enabled: ${enabled}`,
});
await writeData(data);
return { entitlement, data };
}
async function updateService(serviceId, payload, identity) {
return enqueueMutation(async () => {
const data = readData();
@@ -1831,6 +1901,7 @@ export function createControlPlaneStore({ projectRoot }) {
recordTaskManagerWorkspaceMembership,
removeTaskManagerProjectMembership,
removeTaskManagerWorkspaceMembership,
setServiceModuleEntitlement,
setUserServiceAccess,
updateAccessRequest,
updateClient,
@@ -1872,10 +1943,33 @@ function normalizeData(payload) {
}));
data.accessRequests = data.accessRequests.map(normalizeAccessRequest).filter(Boolean);
data.revokedAccounts = data.revokedAccounts.map(normalizeRevokedAccount).filter(Boolean);
data.serviceModuleEntitlements = data.serviceModuleEntitlements.map(normalizeServiceModuleEntitlement).filter(Boolean);
data.taskerInviteRequests = data.taskerInviteRequests.map(normalizeTaskerInviteRequest).filter(Boolean);
return data;
}
function normalizeServiceModuleEntitlement(payload) {
if (typeof payload !== "object" || payload === null) return null;
const clientId = nullableString(payload.clientId);
const serviceId = nullableString(payload.serviceId);
const userId = nullableString(payload.userId);
const moduleId = pickEnum(payload.moduleId, serviceModuleIds, null);
if (!clientId || !serviceId || !userId || !moduleId || payload.enabled === false) return null;
const now = isoNow();
return {
id: optionalString(payload.id, `svc_module_${slugify(`${clientId}-${serviceId}-${userId}-${moduleId}`)}`),
clientId,
serviceId,
userId,
moduleId,
enabled: true,
createdByUserId: nullableStringWithFallback(payload.createdByUserId, null),
createdAt: optionalString(payload.createdAt, now),
updatedAt: optionalString(payload.updatedAt, now),
};
}
function normalizeRevokedAccount(payload) {
if (typeof payload !== "object" || payload === null) return null;
const email = normalizeEmail(payload.email);
+23
View File
@@ -1481,6 +1481,24 @@ app.post("/api/admin/access/user-service", requireLauncherAdmin, asyncRoute(asyn
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
}));
app.post("/api/admin/access/service-modules", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageClient(req, res, req.body?.clientId) || !assertAdminCanManageUser(req, res, req.body?.userId)) {
return;
}
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const service = snapshot.data.services.find((candidate) => candidate.id === req.body?.serviceId);
if (!service) {
res.status(404).json({ ok: false, error: "service_not_found" });
return;
}
const result = await controlPlaneStore.setServiceModuleEntitlement(req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.access.service-module.updated", [req.body?.userId]);
res.json(scopeAdminMutationResult(req, result));
}));
app.post("/api/admin/sync/:syncId/retry", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.retrySync(req.params.syncId, req.nodedcSession.user);
publishControlPlaneEvent("admin.sync.retry");
@@ -3139,6 +3157,9 @@ function scopeControlPlaneData(data, scope) {
return false;
}),
exceptions: data.exceptions.filter((exception) => userIds.has(exception.userId)),
serviceModuleEntitlements: data.serviceModuleEntitlements.filter(
(entitlement) => clientIds.has(entitlement.clientId) && userIds.has(entitlement.userId)
),
taskManagerMemberships: data.taskManagerMemberships.filter(
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
),
@@ -3168,6 +3189,7 @@ function scopeRuntimeControlPlaneData(data, userId) {
taskerInviteRequests: [],
grants: [],
exceptions: [],
serviceModuleEntitlements: [],
syncStatuses: [],
auditEvents: [],
taskManagerMemberships: [],
@@ -3199,6 +3221,7 @@ function scopeRuntimeControlPlaneData(data, userId) {
return false;
}),
exceptions: data.exceptions.filter((exception) => exception.userId === user.id),
serviceModuleEntitlements: data.serviceModuleEntitlements.filter((entitlement) => entitlement.userId === user.id),
syncStatuses: [],
auditEvents: [],
taskManagerMemberships: data.taskManagerMemberships.filter(