feat: add Ops Codex terminal setup UI
This commit is contained in:
parent
df4767a085
commit
763b90f3ba
|
|
@ -1,4 +1,5 @@
|
|||
FROM python:3.12.10-alpine
|
||||
LABEL nodedc.deploy.cache_bust="codex-agent-setup-20260624"
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
|
@ -44,6 +45,7 @@ COPY manage.py manage.py
|
|||
COPY plane plane/
|
||||
COPY templates templates/
|
||||
COPY package.json package.json
|
||||
RUN grep -R "setup-codes" /code/plane/app/urls/codex_agents.py >/dev/null
|
||||
|
||||
COPY ./bin ./bin/
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from plane.app.views import (
|
|||
CodexAgentListEndpoint,
|
||||
CodexAgentProjectAccessEndpoint,
|
||||
CodexAgentRevokeEndpoint,
|
||||
CodexAgentSetupCodeEndpoint,
|
||||
CodexAgentSetupEndpoint,
|
||||
CodexAgentTokenListEndpoint,
|
||||
CodexAgentTokenRevokeEndpoint,
|
||||
|
|
@ -69,4 +70,9 @@ urlpatterns = [
|
|||
CodexAgentSetupEndpoint.as_view(),
|
||||
name="codex-agent-api-agent-setup",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/codex-agent-api/agents/<uuid:agent_id>/setup-codes/",
|
||||
CodexAgentSetupCodeEndpoint.as_view(),
|
||||
name="codex-agent-api-agent-setup-codes",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ from .codex_agents import (
|
|||
CodexAgentListEndpoint,
|
||||
CodexAgentProjectAccessEndpoint,
|
||||
CodexAgentRevokeEndpoint,
|
||||
CodexAgentSetupCodeEndpoint,
|
||||
CodexAgentSetupEndpoint,
|
||||
CodexAgentTokenListEndpoint,
|
||||
CodexAgentTokenRevokeEndpoint,
|
||||
|
|
|
|||
|
|
@ -563,3 +563,21 @@ class CodexAgentSetupEndpoint(CodexAgentEntitledEndpoint):
|
|||
return entitlement_error
|
||||
|
||||
return gateway_request("GET", f"/api/internal/v1/owners/{owner_path(request.user)}/agents/{agent_path(agent_id)}/setup")
|
||||
|
||||
|
||||
class CodexAgentSetupCodeEndpoint(CodexAgentEntitledEndpoint):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def post(self, request, slug, agent_id):
|
||||
entitlement_error = self.require_entitlement(request, slug)
|
||||
if entitlement_error is not None:
|
||||
return entitlement_error
|
||||
|
||||
payload = {
|
||||
"expires_in_seconds": request.data.get("expires_in_seconds") or 600,
|
||||
"token_name": request.data.get("token_name") or "Local Codex token",
|
||||
}
|
||||
return gateway_request(
|
||||
"POST",
|
||||
f"/api/internal/v1/owners/{owner_path(request.user)}/agents/{agent_path(agent_id)}/setup-codes",
|
||||
payload,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
FROM node:22-alpine AS base
|
||||
LABEL nodedc.deploy.cache_bust="codex-agent-setup-20260624"
|
||||
|
||||
# Setup pnpm package manager with corepack and configure global bin directory for caching
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
|
|
@ -37,6 +38,7 @@ RUN corepack enable pnpm
|
|||
# Copy full directory structure before fetch to ensure all package.json files are available
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
RUN grep -R "Recommended terminal setup" /app/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx >/dev/null
|
||||
|
||||
# Fetch dependencies to cache store, then install offline with dev deps
|
||||
RUN pnpm fetch --store-dir=/pnpm/store
|
||||
|
|
@ -93,6 +95,7 @@ FROM nginx:1.27-alpine AS production
|
|||
|
||||
COPY apps/web/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=installer /app/apps/web/build/client /usr/share/nginx/html
|
||||
RUN grep -R "Recommended terminal setup" /usr/share/nginx/html/assets >/dev/null
|
||||
RUN chmod -R a+rX /usr/share/nginx/html
|
||||
|
||||
EXPOSE 3000
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { type ChangeEvent, useMemo, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Bot, Check, ChevronDown, Copy, FolderKanban, KeyRound, Route, ShieldCheck } from "lucide-react";
|
||||
import { Bot, Check, ChevronDown, Copy, FolderKanban, KeyRound, Route, ShieldCheck, Terminal } from "lucide-react";
|
||||
import useSWR from "swr";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
type TCodexAgent,
|
||||
type TCodexAgentGrant,
|
||||
type TCodexAgentGrantableWorkspace,
|
||||
type TCodexAgentSetupCode,
|
||||
type TCodexAgentSetupPacket,
|
||||
type TCodexAgentToken,
|
||||
} from "@/services/workspace-codex-agent.service";
|
||||
|
|
@ -59,7 +60,9 @@ type TProps = {
|
|||
type TAgentSetupCard = {
|
||||
agent: TCodexAgent;
|
||||
grants: TCodexAgentGrant[];
|
||||
installCommand?: string;
|
||||
setup?: TCodexAgentSetupPacket;
|
||||
setupCode?: TCodexAgentSetupCode;
|
||||
tokens: TCodexAgentToken[];
|
||||
};
|
||||
|
||||
|
|
@ -84,6 +87,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
const [createdSetupCards, setCreatedSetupCards] = useState<TAgentSetupCard[]>([]);
|
||||
const [revealedTokens, setRevealedTokens] = useState<Record<string, string>>({});
|
||||
const [updatingAgentIds, setUpdatingAgentIds] = useState<Record<string, boolean>>({});
|
||||
const [creatingSetupCodeAgentIds, setCreatingSetupCodeAgentIds] = useState<Record<string, boolean>>({});
|
||||
const [creatingTokenAgentIds, setCreatingTokenAgentIds] = useState<Record<string, boolean>>({});
|
||||
const [openProjectAccessAgentId, setOpenProjectAccessAgentId] = useState<string | null>(null);
|
||||
const [projectGrantDrafts, setProjectGrantDrafts] = useState<Record<string, string[]>>({});
|
||||
|
|
@ -221,19 +225,20 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
mode: "voluntary",
|
||||
}
|
||||
);
|
||||
const tokenResponse = await codexAgentService.createToken(workspaceSlug, createResponse.agent.id, {
|
||||
name: `${displayName} local token`,
|
||||
});
|
||||
setRevealedTokens((currentTokens) => ({
|
||||
...currentTokens,
|
||||
[tokenResponse.token_record.id]: tokenResponse.token,
|
||||
}));
|
||||
const [setupResponse, setupCodeResponse] = await Promise.all([
|
||||
codexAgentService.getSetup(workspaceSlug, createResponse.agent.id),
|
||||
codexAgentService.createSetupCode(workspaceSlug, createResponse.agent.id, {
|
||||
expires_in_seconds: 600,
|
||||
token_name: `${displayName} local token`,
|
||||
}),
|
||||
]);
|
||||
setCreatedSetupCards((currentCards) =>
|
||||
upsertSetupCardToken(
|
||||
upsertSetupCardSetupCode(
|
||||
currentCards,
|
||||
createResponse.agent,
|
||||
tokenResponse.token_record,
|
||||
tokenResponse.setup,
|
||||
setupCodeResponse.setup_code_record,
|
||||
setupCodeResponse.install.command,
|
||||
setupResponse.setup,
|
||||
grantsResponse.grants
|
||||
)
|
||||
);
|
||||
|
|
@ -242,7 +247,8 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Codex agent создан",
|
||||
message: "Полный token показан только в текущем открытии раздела. После перезахода останется masked suffix.",
|
||||
message:
|
||||
"Скопируйте terminal-команду в локальный терминал. Setup-код действует 10 минут и используется один раз.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
setToast({
|
||||
|
|
@ -255,6 +261,40 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
}
|
||||
};
|
||||
|
||||
const handleCreateSetupCode = async (agent: TCodexAgent) => {
|
||||
setCreatingSetupCodeAgentIds((current) => ({ ...current, [agent.id]: true }));
|
||||
try {
|
||||
const setupCodeResponse = await codexAgentService.createSetupCode(workspaceSlug, agent.id, {
|
||||
expires_in_seconds: 600,
|
||||
token_name: `${agent.display_name} local token`,
|
||||
});
|
||||
const currentCard = setupCards.find((card) => card.agent.id === agent.id);
|
||||
setCreatedSetupCards((currentCards) =>
|
||||
upsertSetupCardSetupCode(
|
||||
currentCards,
|
||||
agent,
|
||||
setupCodeResponse.setup_code_record,
|
||||
setupCodeResponse.install.command,
|
||||
currentCard?.setup,
|
||||
currentCard?.grants ?? []
|
||||
)
|
||||
);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Terminal-команда выпущена",
|
||||
message: "Setup-код действует 10 минут и используется один раз.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Не удалось выпустить terminal-команду",
|
||||
message: error?.message ?? error?.error ?? "Проверьте Gateway и права workspace.",
|
||||
});
|
||||
} finally {
|
||||
setCreatingSetupCodeAgentIds((current) => ({ ...current, [agent.id]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateToken = async (agent: TCodexAgent) => {
|
||||
setCreatingTokenAgentIds((current) => ({ ...current, [agent.id]: true }));
|
||||
try {
|
||||
|
|
@ -435,7 +475,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
<CapabilityCard
|
||||
icon={KeyRound}
|
||||
title="Локальный Codex"
|
||||
description="Пользовательский Codex подключается по MCP endpoint с agent token; token хранится только на стороне Gateway."
|
||||
description="Пользовательский Codex подключается по MCP endpoint через одноразовый setup-code или legacy agent token."
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
|
@ -445,7 +485,8 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-15 font-semibold text-primary">Создать агента workspace</div>
|
||||
<p className="max-w-3xl text-12 leading-5 text-tertiary">
|
||||
Задайте имя, выберите project grant и выпустите agent token. Аватар меняется кликом по кругу.
|
||||
Задайте имя, выберите project grant и получите terminal-команду для локального Codex. Аватар
|
||||
меняется кликом по кругу.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -514,11 +555,14 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
activeAgents.map((agent) => {
|
||||
const draftName = getAgentDraftName(agentDraftNames, agent);
|
||||
const isUpdatingAgent = updatingAgentIds[agent.id] === true;
|
||||
const isCreatingSetupCode = creatingSetupCodeAgentIds[agent.id] === true;
|
||||
const isCreatingToken = creatingTokenAgentIds[agent.id] === true;
|
||||
const isAgentDirty = draftName.trim() !== agent.display_name;
|
||||
const setupCard = setupCards.find((card) => card.agent.id === agent.id);
|
||||
const agentTokens = setupCard?.tokens ?? [];
|
||||
const agentGrants = setupCard?.grants ?? [];
|
||||
const installCommand = setupCard?.installCommand;
|
||||
const setupCode = setupCard?.setupCode;
|
||||
const currentGrantKeys = getGrantedProjectKeys(agentGrants);
|
||||
const draftGrantKeys = projectGrantDrafts[agent.id] ?? currentGrantKeys;
|
||||
const isProjectAccessOpen = openProjectAccessAgentId === agent.id;
|
||||
|
|
@ -584,15 +628,6 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="nodedc-settings-chip h-12"
|
||||
loading={isCreatingToken}
|
||||
onClick={() => void handleCreateToken(agent)}
|
||||
>
|
||||
Новый токен
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
|
@ -604,8 +639,18 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<AgentTerminalSetupPanel
|
||||
command={installCommand}
|
||||
isCreating={isCreatingSetupCode}
|
||||
setupCode={setupCode}
|
||||
onCopyCommand={() => installCommand && void handleCopy(installCommand, "Terminal-команда")}
|
||||
onCreate={() => void handleCreateSetupCode(agent)}
|
||||
/>
|
||||
|
||||
{areSetupCardsLoading && agentTokens.length === 0 ? (
|
||||
<div className="nodedc-settings-field px-4 py-4 text-13 text-secondary">Загрузка токена...</div>
|
||||
<div className="nodedc-settings-field px-4 py-4 text-13 text-secondary">
|
||||
Загрузка legacy-токенов...
|
||||
</div>
|
||||
) : agentTokens.length > 0 ? (
|
||||
<div className="grid gap-4">
|
||||
{agentTokens.map((token) => {
|
||||
|
|
@ -617,7 +662,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
return (
|
||||
<div key={token.id} className="nodedc-settings-field p-4">
|
||||
<div className="mb-2 text-12 font-semibold tracking-wide text-tertiary uppercase">
|
||||
Agent token
|
||||
Legacy agent token
|
||||
</div>
|
||||
<div className="relative">
|
||||
<code className="nodedc-settings-input flex h-12 w-full items-center overflow-hidden px-4 pr-14 text-12 text-primary">
|
||||
|
|
@ -647,8 +692,19 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
|||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="nodedc-settings-field px-4 py-4 text-13 text-secondary">
|
||||
Токен ещё не выпущен. Нажмите «Новый токен», чтобы получить доступ для локального Codex.
|
||||
<div className="nodedc-settings-field flex flex-col gap-3 px-4 py-4 text-13 text-secondary sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>
|
||||
Legacy-токен ещё не выпущен. Используйте его только для ручной настройки config.toml.
|
||||
</span>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="nodedc-settings-save-button shrink-0"
|
||||
loading={isCreatingToken}
|
||||
onClick={() => void handleCreateToken(agent)}
|
||||
>
|
||||
Сгенерировать токен
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -704,12 +760,74 @@ type TCodexConnectionGuideProps = {
|
|||
onCopyConfig: () => void;
|
||||
};
|
||||
|
||||
type TAgentTerminalSetupPanelProps = {
|
||||
command?: string;
|
||||
isCreating: boolean;
|
||||
onCopyCommand: () => void;
|
||||
onCreate: () => void;
|
||||
setupCode?: TCodexAgentSetupCode;
|
||||
};
|
||||
|
||||
function AgentTerminalSetupPanel(props: TAgentTerminalSetupPanelProps) {
|
||||
const expiresAtLabel = props.setupCode?.expires_at ? new Date(props.setupCode.expires_at).toLocaleString() : null;
|
||||
|
||||
return (
|
||||
<div className="nodedc-settings-field p-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="grid size-10 shrink-0 place-items-center rounded-full bg-white/8 text-tertiary">
|
||||
<Terminal className="size-4" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="text-13 font-semibold text-primary">Recommended terminal setup</div>
|
||||
<div className="mt-1 text-12 text-tertiary">
|
||||
Одноразовая npm-команда для локального терминала: создаст token, обновит Codex config и установит
|
||||
ops-context skill.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={props.command ? "secondary" : "primary"}
|
||||
size="sm"
|
||||
className={props.command ? "nodedc-settings-chip" : "nodedc-settings-save-button"}
|
||||
loading={props.isCreating}
|
||||
onClick={props.onCreate}
|
||||
>
|
||||
{props.command ? "Обновить команду" : "Получить команду"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{props.command ? (
|
||||
<div className="mt-4 grid gap-3">
|
||||
<pre className="nodedc-settings-input font-mono max-h-40 w-full overflow-auto px-3 py-3 text-12 leading-5 break-all whitespace-pre-wrap text-primary">
|
||||
{props.command}
|
||||
</pre>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="text-12 text-tertiary">
|
||||
{expiresAtLabel ? `Действует до ${expiresAtLabel}. ` : ""}
|
||||
Команда не показывает agent token, использует npm package installer и сработает только один раз.
|
||||
</div>
|
||||
<Button variant="primary" size="sm" className="nodedc-settings-save-button" onClick={props.onCopyCommand}>
|
||||
<Copy className="mr-2 size-3.5" />
|
||||
Скопировать команду
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 rounded-2xl bg-white/[0.03] px-4 py-3 text-12 leading-5 text-tertiary">
|
||||
Нажмите «Получить команду» и вставьте ее в терминал на машине, где запущен локальный Codex.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
|
||||
return (
|
||||
<div className="grid gap-4 p-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-14 font-semibold text-primary">Подключение локального Codex</div>
|
||||
<div className="text-14 font-semibold text-primary">Legacy manual setup</div>
|
||||
</div>
|
||||
<span className="nodedc-settings-chip inline-flex min-h-11 w-fit items-center justify-center text-12">
|
||||
MCP endpoint · {props.mcpEndpoint}
|
||||
|
|
@ -763,7 +881,7 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
|
|||
<p className="mb-3 text-13 leading-5 text-secondary">
|
||||
Добавьте этот блок в конец существующего <code>config.toml</code>. Не заменяйте файл целиком. Если блок{" "}
|
||||
<code>[mcp_servers.{CODEX_MCP_SERVER_NAME}]</code> уже есть — замените только этот блок и его{" "}
|
||||
<code>headers</code>.
|
||||
<code>http_headers</code>.
|
||||
</p>
|
||||
<pre className="nodedc-settings-input font-mono w-full px-3 py-3 text-12 leading-5 break-all whitespace-pre-wrap text-primary">
|
||||
{props.configSnippet}
|
||||
|
|
@ -790,7 +908,7 @@ required = false
|
|||
startup_timeout_sec = 20
|
||||
tool_timeout_sec = 60
|
||||
|
||||
[mcp_servers.${CODEX_MCP_SERVER_NAME}.headers]
|
||||
[mcp_servers.${CODEX_MCP_SERVER_NAME}.http_headers]
|
||||
Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
|
||||
Accept = "application/json"
|
||||
"MCP-Protocol-Version" = "2025-06-18"`;
|
||||
|
|
@ -994,12 +1112,14 @@ function mergeSetupCards(persistedCards: TAgentSetupCard[], createdCards: TAgent
|
|||
cardsByAgentId.set(card.agent.id, {
|
||||
agent: persistedCard.agent,
|
||||
grants: mergeAgentGrants(persistedCard.grants, card.grants),
|
||||
installCommand: card.installCommand ?? persistedCard.installCommand,
|
||||
setup: persistedCard.setup ?? card.setup,
|
||||
setupCode: card.setupCode ?? persistedCard.setupCode,
|
||||
tokens: mergeTokens(persistedCard.tokens, card.tokens),
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(cardsByAgentId.values()).filter((card) => card.tokens.length > 0);
|
||||
return Array.from(cardsByAgentId.values());
|
||||
}
|
||||
|
||||
function mergeTokens(primaryTokens: TCodexAgentToken[], secondaryTokens: TCodexAgentToken[]): TCodexAgentToken[] {
|
||||
|
|
@ -1029,13 +1149,42 @@ function upsertSetupCardToken(
|
|||
? {
|
||||
agent,
|
||||
grants: mergeAgentGrants(card.grants, grants),
|
||||
installCommand: card.installCommand,
|
||||
setup: setup ?? card.setup,
|
||||
setupCode: card.setupCode,
|
||||
tokens: mergeTokens([token], card.tokens),
|
||||
}
|
||||
: card
|
||||
);
|
||||
}
|
||||
|
||||
function upsertSetupCardSetupCode(
|
||||
cards: TAgentSetupCard[],
|
||||
agent: TCodexAgent,
|
||||
setupCode: TCodexAgentSetupCode,
|
||||
installCommand: string,
|
||||
setup?: TCodexAgentSetupPacket,
|
||||
grants: TCodexAgentGrant[] = []
|
||||
): TAgentSetupCard[] {
|
||||
const existingCard = cards.find((card) => card.agent.id === agent.id);
|
||||
if (!existingCard) {
|
||||
return [{ agent, grants, installCommand, setup, setupCode, tokens: [] }, ...cards];
|
||||
}
|
||||
|
||||
return cards.map((card) =>
|
||||
card.agent.id === agent.id
|
||||
? {
|
||||
agent,
|
||||
grants: mergeAgentGrants(card.grants, grants),
|
||||
installCommand,
|
||||
setup: setup ?? card.setup,
|
||||
setupCode,
|
||||
tokens: card.tokens,
|
||||
}
|
||||
: card
|
||||
);
|
||||
}
|
||||
|
||||
function flattenProjectAccessOptions(workspaceGroups: TCodexAgentGrantableWorkspace[]): TProjectAccessOption[] {
|
||||
return workspaceGroups.flatMap((workspaceGroup) =>
|
||||
workspaceGroup.projects.map((project) => ({
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { APIService } from "@/services/api.service";
|
|||
export type TCodexAgentStatus = "active" | "disabled" | "revoked";
|
||||
export type TCodexAgentGrantMode = "voluntary" | "reporting";
|
||||
export type TCodexAgentTokenStatus = "active" | "revoked" | "expired";
|
||||
export type TCodexAgentSetupCodeStatus = "active" | "used" | "expired" | "revoked";
|
||||
|
||||
export type TCodexAgent = {
|
||||
id: string;
|
||||
|
|
@ -45,6 +46,19 @@ export type TCodexAgentToken = {
|
|||
created_at: string;
|
||||
};
|
||||
|
||||
export type TCodexAgentSetupCode = {
|
||||
id: string;
|
||||
agent_id: string;
|
||||
code_suffix: string | null;
|
||||
status: TCodexAgentSetupCodeStatus;
|
||||
token_name: string;
|
||||
created_by_user_id: string | null;
|
||||
expires_at: string;
|
||||
used_at: string | null;
|
||||
used_token_id: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type TCodexAgentSetupPacket = {
|
||||
agents_md?: string;
|
||||
codex_config_template?: Record<string, unknown>;
|
||||
|
|
@ -67,6 +81,25 @@ export type TCodexAgentCreateTokenResponse = {
|
|||
token_record: TCodexAgentToken;
|
||||
};
|
||||
|
||||
export type TCodexAgentCreateSetupCodeResponse = {
|
||||
ok: boolean;
|
||||
install: {
|
||||
command: string;
|
||||
fallback_command?: string;
|
||||
installer_kind?: "npm_registry" | "npm_tarball" | "python_script";
|
||||
installer_url: string;
|
||||
legacy_python_installer_url?: string;
|
||||
mcp_server_name: string;
|
||||
npm_spec?: string;
|
||||
package_url?: string;
|
||||
platform: "macos_linux";
|
||||
registry_command?: string;
|
||||
skill_name: string;
|
||||
};
|
||||
setup_code: string;
|
||||
setup_code_record: TCodexAgentSetupCode;
|
||||
};
|
||||
|
||||
export type TCodexAgentTokenListResponse = {
|
||||
ok: boolean;
|
||||
tokens: TCodexAgentToken[];
|
||||
|
|
@ -222,6 +255,18 @@ export class WorkspaceCodexAgentService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async createSetupCode(
|
||||
workspaceSlug: string,
|
||||
agentId: string,
|
||||
data: { expires_in_seconds?: number; token_name?: string }
|
||||
): Promise<TCodexAgentCreateSetupCodeResponse> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/codex-agent-api/agents/${agentId}/setup-codes/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async listTokens(workspaceSlug: string, agentId: string): Promise<TCodexAgentTokenListResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/codex-agent-api/agents/${agentId}/tokens/`)
|
||||
.then((response) => response?.data)
|
||||
|
|
|
|||
Loading…
Reference in New Issue