FEAT - TASKER CODEX: user-scoped multi-workspace grants
This commit is contained in:
+175
-86
@@ -16,11 +16,11 @@ import { SettingsHeading } from "@/components/settings/heading";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project/project.service";
|
||||
import {
|
||||
WorkspaceCodexAgentService,
|
||||
type TCodexAgent,
|
||||
type TCodexAgentGrant,
|
||||
type TCodexAgentGrantableWorkspace,
|
||||
type TCodexAgentSetupPacket,
|
||||
type TCodexAgentToken,
|
||||
} from "@/services/workspace-codex-agent.service";
|
||||
@@ -49,7 +49,6 @@ const CODEX_MCP_SERVER_NAME = "nodedc-ops-agent";
|
||||
const DEFAULT_OPS_AGENT_MCP_ENDPOINT = "https://ops-agents.nodedc.ru/mcp";
|
||||
|
||||
const codexAgentService = new WorkspaceCodexAgentService();
|
||||
const projectService = new ProjectService();
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
type TProps = {
|
||||
@@ -67,7 +66,10 @@ type TAgentSetupCard = {
|
||||
type TProjectAccessOption = {
|
||||
id: string;
|
||||
identifier?: string | null;
|
||||
key: string;
|
||||
name: string;
|
||||
workspaceName: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSettingsContent(props: TProps) {
|
||||
@@ -77,7 +79,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
|
||||
const [newAgentName, setNewAgentName] = useState("Local Codex");
|
||||
const [newAgentAvatarUrl, setNewAgentAvatarUrl] = useState<string | null>(null);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState("");
|
||||
const [selectedProjectKey, setSelectedProjectKey] = useState("");
|
||||
const [agentDraftNames, setAgentDraftNames] = useState<Record<string, string>>({});
|
||||
const [createdSetupCards, setCreatedSetupCards] = useState<TAgentSetupCard[]>([]);
|
||||
const [revealedTokens, setRevealedTokens] = useState<Record<string, string>>({});
|
||||
@@ -100,8 +102,9 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
} = useSWR(isCodexAgentEntitled ? `CODEX_AGENT_API_AGENTS_${workspaceSlug}` : null, () =>
|
||||
codexAgentService.listAgents(workspaceSlug)
|
||||
);
|
||||
const { data: projects } = useSWR(isCodexAgentEntitled ? `CODEX_AGENT_API_PROJECTS_${workspaceSlug}` : null, () =>
|
||||
projectService.getProjectsLite(workspaceSlug)
|
||||
const { data: projectAccessPayload } = useSWR(
|
||||
isCodexAgentEntitled ? `CODEX_AGENT_API_PROJECT_ACCESS_${workspaceSlug}` : null,
|
||||
() => codexAgentService.listProjectAccess(workspaceSlug)
|
||||
);
|
||||
const activeAgents = useMemo(
|
||||
() => (codexAgentsPayload?.agents ?? []).filter((agent) => agent.status !== "revoked"),
|
||||
@@ -134,8 +137,9 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
})
|
||||
)
|
||||
);
|
||||
const projectOptions = projects ?? [];
|
||||
const effectiveSelectedProjectId = selectedProjectId || projectOptions[0]?.id || "";
|
||||
const workspaceProjectGroups = projectAccessPayload?.workspaces ?? [];
|
||||
const projectOptions = useMemo(() => flattenProjectAccessOptions(workspaceProjectGroups), [workspaceProjectGroups]);
|
||||
const effectiveSelectedProjectKey = selectedProjectKey || projectOptions[0]?.key || "";
|
||||
const setupCards = useMemo(
|
||||
() => mergeSetupCards(persistedSetupCards ?? [], createdSetupCards),
|
||||
[createdSetupCards, persistedSetupCards]
|
||||
@@ -191,7 +195,8 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
|
||||
const handleCreateAgent = async () => {
|
||||
const displayName = newAgentName.trim();
|
||||
if (!displayName || !effectiveSelectedProjectId) return;
|
||||
const initialGrant = parseProjectGrantKey(effectiveSelectedProjectKey);
|
||||
if (!displayName || !initialGrant) return;
|
||||
|
||||
setIsCreatingAgent(true);
|
||||
try {
|
||||
@@ -199,11 +204,20 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
display_name: displayName,
|
||||
avatar_url: newAgentAvatarUrl,
|
||||
});
|
||||
const grantResponse = await codexAgentService.upsertGrant(workspaceSlug, createResponse.agent.id, {
|
||||
project_id: effectiveSelectedProjectId,
|
||||
scopes: TASK_AUTHOR_SCOPES,
|
||||
mode: "voluntary",
|
||||
});
|
||||
const grantsResponse = await codexAgentService.replaceProjectGrantsAcrossWorkspaces(
|
||||
workspaceSlug,
|
||||
createResponse.agent.id,
|
||||
{
|
||||
grants: [
|
||||
{
|
||||
workspace_slug: initialGrant.workspaceSlug,
|
||||
project_id: initialGrant.projectId,
|
||||
},
|
||||
],
|
||||
scopes: TASK_AUTHOR_SCOPES,
|
||||
mode: "voluntary",
|
||||
}
|
||||
);
|
||||
const tokenResponse = await codexAgentService.createToken(workspaceSlug, createResponse.agent.id, {
|
||||
name: `${displayName} local token`,
|
||||
});
|
||||
@@ -212,9 +226,13 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
[tokenResponse.token_record.id]: tokenResponse.token,
|
||||
}));
|
||||
setCreatedSetupCards((currentCards) =>
|
||||
upsertSetupCardToken(currentCards, createResponse.agent, tokenResponse.token_record, tokenResponse.setup, [
|
||||
grantResponse.grant,
|
||||
])
|
||||
upsertSetupCardToken(
|
||||
currentCards,
|
||||
createResponse.agent,
|
||||
tokenResponse.token_record,
|
||||
tokenResponse.setup,
|
||||
grantsResponse.grants
|
||||
)
|
||||
);
|
||||
await mutateCodexAgents();
|
||||
await mutateSetupCards();
|
||||
@@ -265,35 +283,35 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleProjectGrant = (agentId: string, currentProjectIds: string[], projectId: string) => {
|
||||
const handleToggleProjectGrant = (agentId: string, currentGrantKeys: string[], grantKey: string) => {
|
||||
setProjectGrantDrafts((currentDrafts) => {
|
||||
const currentDraftProjectIds = currentDrafts[agentId] ?? currentProjectIds;
|
||||
const nextProjectIds = currentDraftProjectIds.includes(projectId)
|
||||
? currentDraftProjectIds.filter((currentProjectId) => currentProjectId !== projectId)
|
||||
: [...currentDraftProjectIds, projectId];
|
||||
const currentDraftGrantKeys = currentDrafts[agentId] ?? currentGrantKeys;
|
||||
const nextGrantKeys = currentDraftGrantKeys.includes(grantKey)
|
||||
? currentDraftGrantKeys.filter((currentGrantKey) => currentGrantKey !== grantKey)
|
||||
: [...currentDraftGrantKeys, grantKey];
|
||||
|
||||
return {
|
||||
...currentDrafts,
|
||||
[agentId]: nextProjectIds,
|
||||
[agentId]: nextGrantKeys,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveProjectAccess = async (agent: TCodexAgent, selectedProjectIds: string[]) => {
|
||||
const projectIds = [...new Set(selectedProjectIds.filter(Boolean))];
|
||||
if (projectIds.length === 0) {
|
||||
const handleSaveProjectAccess = async (agent: TCodexAgent, selectedGrantKeys: string[]) => {
|
||||
const projectGrants = buildProjectGrantPayload(selectedGrantKeys);
|
||||
if (projectGrants.length === 0) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Выберите project",
|
||||
message: "У агента должен быть доступ хотя бы к одному project в workspace.",
|
||||
message: "У агента должен быть доступ хотя бы к одному project.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingProjectGrantAgentIds((current) => ({ ...current, [agent.id]: true }));
|
||||
try {
|
||||
const grantsResponse = await codexAgentService.replaceProjectGrants(workspaceSlug, agent.id, {
|
||||
project_ids: projectIds,
|
||||
const grantsResponse = await codexAgentService.replaceProjectGrantsAcrossWorkspaces(workspaceSlug, agent.id, {
|
||||
grants: projectGrants,
|
||||
scopes: TASK_AUTHOR_SCOPES,
|
||||
mode: "voluntary",
|
||||
});
|
||||
@@ -302,7 +320,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
card.agent.id === agent.id
|
||||
? {
|
||||
...card,
|
||||
grants: mergeAgentGrants(card.grants, grantsResponse.grants, workspaceSlug),
|
||||
grants: mergeAgentGrants(card.grants, grantsResponse.grants, { replaceAllProjectGrants: true }),
|
||||
}
|
||||
: card
|
||||
)
|
||||
@@ -316,7 +334,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Доступы Codex обновлены",
|
||||
message: "Agent token теперь работает только с выбранными projects.",
|
||||
message: "Agent token теперь работает только с выбранными workspace/projects.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
setToast({
|
||||
@@ -457,12 +475,12 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
<span className="text-body-sm-medium text-tertiary">Выберите project</span>
|
||||
<select
|
||||
className="nodedc-settings-select h-12 w-full px-4 text-13"
|
||||
value={effectiveSelectedProjectId}
|
||||
onChange={(event) => setSelectedProjectId(event.target.value)}
|
||||
value={effectiveSelectedProjectKey}
|
||||
onChange={(event) => setSelectedProjectKey(event.target.value)}
|
||||
>
|
||||
{projectOptions.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
<option key={project.key} value={project.key}>
|
||||
{project.workspaceName} · {project.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -471,7 +489,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="nodedc-settings-save-button h-12 min-w-[11rem] self-end px-5"
|
||||
disabled={!newAgentName.trim() || !effectiveSelectedProjectId}
|
||||
disabled={!newAgentName.trim() || !effectiveSelectedProjectKey}
|
||||
loading={isCreatingAgent}
|
||||
onClick={handleCreateAgent}
|
||||
>
|
||||
@@ -498,8 +516,8 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
const setupCard = setupCards.find((card) => card.agent.id === agent.id);
|
||||
const agentTokens = setupCard?.tokens ?? [];
|
||||
const agentGrants = setupCard?.grants ?? [];
|
||||
const currentProjectIds = getGrantedProjectIds(agentGrants, workspaceSlug);
|
||||
const draftProjectIds = projectGrantDrafts[agent.id] ?? currentProjectIds;
|
||||
const currentGrantKeys = getGrantedProjectKeys(agentGrants);
|
||||
const draftGrantKeys = projectGrantDrafts[agent.id] ?? currentGrantKeys;
|
||||
const isProjectAccessOpen = openProjectAccessAgentId === agent.id;
|
||||
const isSavingProjectAccess = savingProjectGrantAgentIds[agent.id] === true;
|
||||
|
||||
@@ -632,12 +650,12 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
)}
|
||||
|
||||
<AgentProjectAccessPanel
|
||||
currentProjectIds={currentProjectIds}
|
||||
draftProjectIds={draftProjectIds}
|
||||
currentGrantKeys={currentGrantKeys}
|
||||
draftGrantKeys={draftGrantKeys}
|
||||
isOpen={isProjectAccessOpen}
|
||||
isSaving={isSavingProjectAccess}
|
||||
projects={projectOptions}
|
||||
onSave={() => void handleSaveProjectAccess(agent, draftProjectIds)}
|
||||
workspaceGroups={workspaceProjectGroups}
|
||||
onSave={() => void handleSaveProjectAccess(agent, draftGrantKeys)}
|
||||
onToggleOpen={() => {
|
||||
setOpenProjectAccessAgentId(isProjectAccessOpen ? null : agent.id);
|
||||
setProjectGrantDrafts((currentDrafts) =>
|
||||
@@ -645,13 +663,11 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
? currentDrafts
|
||||
: {
|
||||
...currentDrafts,
|
||||
[agent.id]: currentProjectIds,
|
||||
[agent.id]: currentGrantKeys,
|
||||
}
|
||||
);
|
||||
}}
|
||||
onToggleProject={(projectId) =>
|
||||
handleToggleProjectGrant(agent.id, currentProjectIds, projectId)
|
||||
}
|
||||
onToggleProject={(grantKey) => handleToggleProjectGrant(agent.id, currentGrantKeys, grantKey)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@@ -828,19 +844,20 @@ function AgentAvatarButton(props: {
|
||||
}
|
||||
|
||||
type TAgentProjectAccessPanelProps = {
|
||||
currentProjectIds: string[];
|
||||
draftProjectIds: string[];
|
||||
currentGrantKeys: string[];
|
||||
draftGrantKeys: string[];
|
||||
isOpen: boolean;
|
||||
isSaving: boolean;
|
||||
onSave: () => void;
|
||||
onToggleOpen: () => void;
|
||||
onToggleProject: (projectId: string) => void;
|
||||
projects: TProjectAccessOption[];
|
||||
onToggleProject: (grantKey: string) => void;
|
||||
workspaceGroups: TCodexAgentGrantableWorkspace[];
|
||||
};
|
||||
|
||||
function AgentProjectAccessPanel(props: TAgentProjectAccessPanelProps) {
|
||||
const isDirty = !areProjectSelectionsEqual(props.currentProjectIds, props.draftProjectIds);
|
||||
const selectedCount = props.draftProjectIds.length;
|
||||
const isDirty = !areProjectSelectionsEqual(props.currentGrantKeys, props.draftGrantKeys);
|
||||
const selectedCount = props.draftGrantKeys.length;
|
||||
const projectsCount = props.workspaceGroups.reduce((count, group) => count + group.projects.length, 0);
|
||||
const summary =
|
||||
selectedCount === 0
|
||||
? "Нет выбранных projects"
|
||||
@@ -856,9 +873,9 @@ function AgentProjectAccessPanel(props: TAgentProjectAccessPanelProps) {
|
||||
<FolderKanban className="size-4" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="text-13 font-semibold text-primary">Доступы к проектам</div>
|
||||
<div className="text-13 font-semibold text-primary">Доступы к workspace и projects</div>
|
||||
<div className="mt-1 text-12 text-tertiary">
|
||||
Выберите projects, куда этот agent token может читать и писать карточки.
|
||||
Выберите workspaces/projects, куда этот agent token может читать и писать карточки.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -874,44 +891,63 @@ function AgentProjectAccessPanel(props: TAgentProjectAccessPanelProps) {
|
||||
|
||||
{props.isOpen && (
|
||||
<div className="nodedc-project-grants-surface mt-4 rounded-3xl p-3">
|
||||
{props.projects.length > 0 ? (
|
||||
<div className="grid max-h-80 gap-0 overflow-y-auto pr-1">
|
||||
{props.projects.map((project) => {
|
||||
const isChecked = props.draftProjectIds.includes(project.id);
|
||||
{projectsCount > 0 ? (
|
||||
<div className="grid max-h-96 gap-3 overflow-y-auto pr-1">
|
||||
{props.workspaceGroups.map((workspaceGroup) => {
|
||||
if (workspaceGroup.projects.length === 0) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={project.id}
|
||||
type="button"
|
||||
className="nodedc-project-grants-row flex min-h-12 items-center justify-between gap-3 rounded-2xl px-3 py-2 text-left outline-none focus:outline-none focus-visible:ring-0 focus-visible:outline-none"
|
||||
onClick={() => props.onToggleProject(project.id)}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-13 font-medium text-primary">{project.name}</span>
|
||||
{project.identifier && (
|
||||
<span className="mt-0.5 block truncate text-11 text-tertiary">{project.identifier}</span>
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`nodedc-project-grants-check grid size-5 shrink-0 place-items-center rounded-full transition ${
|
||||
isChecked
|
||||
? "bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]"
|
||||
: "bg-white/5 text-transparent"
|
||||
}`}
|
||||
>
|
||||
<Check className="size-3.5" />
|
||||
</span>
|
||||
</button>
|
||||
<div key={workspaceGroup.slug} className="nodedc-project-grants-workspace rounded-2xl p-2">
|
||||
<div className="px-1 pb-1.5">
|
||||
<div className="truncate text-12 font-semibold text-primary">{workspaceGroup.name}</div>
|
||||
<div className="mt-0.5 truncate text-10 tracking-wide text-tertiary uppercase">
|
||||
{workspaceGroup.slug}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-0">
|
||||
{workspaceGroup.projects.map((project) => {
|
||||
const grantKey = buildProjectGrantKey(workspaceGroup.slug, project.id);
|
||||
const isChecked = props.draftGrantKeys.includes(grantKey);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={grantKey}
|
||||
type="button"
|
||||
className="nodedc-project-grants-row flex min-h-12 items-center justify-between gap-3 rounded-2xl px-3 py-2 text-left outline-none focus:outline-none focus-visible:ring-0 focus-visible:outline-none"
|
||||
onClick={() => props.onToggleProject(grantKey)}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-13 font-medium text-primary">{project.name}</span>
|
||||
{project.identifier && (
|
||||
<span className="mt-0.5 block truncate text-11 text-tertiary">
|
||||
{project.identifier}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`nodedc-project-grants-check grid size-5 shrink-0 place-items-center rounded-full transition ${
|
||||
isChecked
|
||||
? "bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]"
|
||||
: "bg-white/5 text-transparent"
|
||||
}`}
|
||||
>
|
||||
<Check className="size-3.5" />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-3 py-4 text-13 text-secondary">В workspace нет доступных projects.</div>
|
||||
<div className="px-3 py-4 text-13 text-secondary">Нет доступных workspace/projects.</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex flex-col gap-2 pt-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="text-12 text-tertiary">
|
||||
Сохранение заменяет grants текущего workspace: снятые галочки сразу отзывают доступ.
|
||||
Сохранение заменяет grants выбранных workspace/projects: снятые галочки сразу отзывают доступ.
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -988,25 +1024,78 @@ function upsertSetupCardToken(
|
||||
);
|
||||
}
|
||||
|
||||
function getGrantedProjectIds(grants: TCodexAgentGrant[], workspaceSlug: string): string[] {
|
||||
function flattenProjectAccessOptions(workspaceGroups: TCodexAgentGrantableWorkspace[]): TProjectAccessOption[] {
|
||||
return workspaceGroups.flatMap((workspaceGroup) =>
|
||||
workspaceGroup.projects.map((project) => ({
|
||||
id: project.id,
|
||||
identifier: project.identifier,
|
||||
key: buildProjectGrantKey(workspaceGroup.slug, project.id),
|
||||
name: project.name,
|
||||
workspaceName: workspaceGroup.name,
|
||||
workspaceSlug: workspaceGroup.slug,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
function buildProjectGrantKey(workspaceSlug: string, projectId: string): string {
|
||||
return `${workspaceSlug}:${projectId}`;
|
||||
}
|
||||
|
||||
function parseProjectGrantKey(grantKey: string): { workspaceSlug: string; projectId: string } | null {
|
||||
const separatorIndex = grantKey.indexOf(":");
|
||||
if (separatorIndex <= 0 || separatorIndex >= grantKey.length - 1) return null;
|
||||
|
||||
return {
|
||||
workspaceSlug: grantKey.slice(0, separatorIndex),
|
||||
projectId: grantKey.slice(separatorIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function buildProjectGrantPayload(grantKeys: string[]): { workspace_slug: string; project_id: string }[] {
|
||||
const seenKeys = new Set<string>();
|
||||
const grants: { workspace_slug: string; project_id: string }[] = [];
|
||||
|
||||
for (const grantKey of grantKeys) {
|
||||
if (seenKeys.has(grantKey)) continue;
|
||||
seenKeys.add(grantKey);
|
||||
|
||||
const parsedGrant = parseProjectGrantKey(grantKey);
|
||||
if (!parsedGrant) continue;
|
||||
|
||||
grants.push({
|
||||
workspace_slug: parsedGrant.workspaceSlug,
|
||||
project_id: parsedGrant.projectId,
|
||||
});
|
||||
}
|
||||
|
||||
return grants;
|
||||
}
|
||||
|
||||
function getGrantedProjectKeys(grants: TCodexAgentGrant[]): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
grants
|
||||
.filter((grant) => grant.workspace_slug === workspaceSlug && grant.project_id)
|
||||
.map((grant) => String(grant.project_id))
|
||||
.filter((grant) => grant.project_id)
|
||||
.map((grant) => buildProjectGrantKey(grant.workspace_slug, String(grant.project_id)))
|
||||
),
|
||||
].sort();
|
||||
}
|
||||
|
||||
type TMergeAgentGrantsOptions = {
|
||||
replaceAllProjectGrants?: boolean;
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
function mergeAgentGrants(
|
||||
currentGrants: TCodexAgentGrant[],
|
||||
nextGrants: TCodexAgentGrant[],
|
||||
workspaceSlug?: string
|
||||
options: TMergeAgentGrantsOptions = {}
|
||||
): TCodexAgentGrant[] {
|
||||
const grantsByKey = new Map<string, TCodexAgentGrant>();
|
||||
|
||||
for (const grant of currentGrants) {
|
||||
if (workspaceSlug && grant.workspace_slug === workspaceSlug) continue;
|
||||
if (options.replaceAllProjectGrants && grant.project_id) continue;
|
||||
if (options.workspaceSlug && grant.workspace_slug === options.workspaceSlug) continue;
|
||||
grantsByKey.set(buildGrantKey(grant), grant);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,31 @@ export type TCodexAgentGrantListResponse = {
|
||||
grants: TCodexAgentGrant[];
|
||||
};
|
||||
|
||||
export type TCodexAgentProjectGrantInput = {
|
||||
project_id: string;
|
||||
workspace_slug: string;
|
||||
};
|
||||
|
||||
export type TCodexAgentGrantableProject = {
|
||||
id: string;
|
||||
workspace_slug: string;
|
||||
name: string;
|
||||
identifier?: string | null;
|
||||
};
|
||||
|
||||
export type TCodexAgentGrantableWorkspace = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
role: number;
|
||||
projects: TCodexAgentGrantableProject[];
|
||||
};
|
||||
|
||||
export type TCodexAgentProjectAccessResponse = {
|
||||
ok: boolean;
|
||||
workspaces: TCodexAgentGrantableWorkspace[];
|
||||
};
|
||||
|
||||
export type TCodexAgentSetupResponse = {
|
||||
ok: boolean;
|
||||
setup?: TCodexAgentSetupPacket;
|
||||
@@ -95,6 +120,14 @@ export class WorkspaceCodexAgentService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async listProjectAccess(workspaceSlug: string): Promise<TCodexAgentProjectAccessResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/codex-agent-api/project-access/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createAgent(
|
||||
workspaceSlug: string,
|
||||
data: { display_name: string; avatar_url?: string | null }
|
||||
@@ -158,6 +191,25 @@ export class WorkspaceCodexAgentService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async replaceProjectGrantsAcrossWorkspaces(
|
||||
workspaceSlug: string,
|
||||
agentId: string,
|
||||
data: {
|
||||
grants: TCodexAgentProjectGrantInput[];
|
||||
mode?: TCodexAgentGrantMode;
|
||||
scopes: string[];
|
||||
}
|
||||
): Promise<TCodexAgentGrantListResponse> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/codex-agent-api/agents/${agentId}/grants/replace-projects/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createToken(
|
||||
workspaceSlug: string,
|
||||
agentId: string,
|
||||
|
||||
@@ -2236,6 +2236,15 @@
|
||||
|
||||
.nodedc-project-grants-surface {
|
||||
background: rgba(0, 0, 0, 0.1) !important;
|
||||
background-image: none !important;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.nodedc-project-grants-workspace {
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
@@ -2247,6 +2256,7 @@
|
||||
.nodedc-project-grants-row:focus-visible,
|
||||
.nodedc-project-grants-row:active {
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
Reference in New Issue
Block a user