FEAT - LAUNCHER: модульный доступ Codex Agent API
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user