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
|
FROM python:3.12.10-alpine
|
||||||
|
LABEL nodedc.deploy.cache_bust="codex-agent-setup-20260624"
|
||||||
|
|
||||||
# set environment variables
|
# set environment variables
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
@ -44,6 +45,7 @@ COPY manage.py manage.py
|
||||||
COPY plane plane/
|
COPY plane plane/
|
||||||
COPY templates templates/
|
COPY templates templates/
|
||||||
COPY package.json package.json
|
COPY package.json package.json
|
||||||
|
RUN grep -R "setup-codes" /code/plane/app/urls/codex_agents.py >/dev/null
|
||||||
|
|
||||||
COPY ./bin ./bin/
|
COPY ./bin ./bin/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from plane.app.views import (
|
||||||
CodexAgentListEndpoint,
|
CodexAgentListEndpoint,
|
||||||
CodexAgentProjectAccessEndpoint,
|
CodexAgentProjectAccessEndpoint,
|
||||||
CodexAgentRevokeEndpoint,
|
CodexAgentRevokeEndpoint,
|
||||||
|
CodexAgentSetupCodeEndpoint,
|
||||||
CodexAgentSetupEndpoint,
|
CodexAgentSetupEndpoint,
|
||||||
CodexAgentTokenListEndpoint,
|
CodexAgentTokenListEndpoint,
|
||||||
CodexAgentTokenRevokeEndpoint,
|
CodexAgentTokenRevokeEndpoint,
|
||||||
|
|
@ -69,4 +70,9 @@ urlpatterns = [
|
||||||
CodexAgentSetupEndpoint.as_view(),
|
CodexAgentSetupEndpoint.as_view(),
|
||||||
name="codex-agent-api-agent-setup",
|
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,
|
CodexAgentListEndpoint,
|
||||||
CodexAgentProjectAccessEndpoint,
|
CodexAgentProjectAccessEndpoint,
|
||||||
CodexAgentRevokeEndpoint,
|
CodexAgentRevokeEndpoint,
|
||||||
|
CodexAgentSetupCodeEndpoint,
|
||||||
CodexAgentSetupEndpoint,
|
CodexAgentSetupEndpoint,
|
||||||
CodexAgentTokenListEndpoint,
|
CodexAgentTokenListEndpoint,
|
||||||
CodexAgentTokenRevokeEndpoint,
|
CodexAgentTokenRevokeEndpoint,
|
||||||
|
|
|
||||||
|
|
@ -563,3 +563,21 @@ class CodexAgentSetupEndpoint(CodexAgentEntitledEndpoint):
|
||||||
return entitlement_error
|
return entitlement_error
|
||||||
|
|
||||||
return gateway_request("GET", f"/api/internal/v1/owners/{owner_path(request.user)}/agents/{agent_path(agent_id)}/setup")
|
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
|
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
|
# Setup pnpm package manager with corepack and configure global bin directory for caching
|
||||||
ENV PNPM_HOME="/pnpm"
|
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 full directory structure before fetch to ensure all package.json files are available
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
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
|
# Fetch dependencies to cache store, then install offline with dev deps
|
||||||
RUN pnpm fetch --store-dir=/pnpm/store
|
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 apps/web/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY --from=installer /app/apps/web/build/client /usr/share/nginx/html
|
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
|
RUN chmod -R a+rX /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { type ChangeEvent, useMemo, useRef, useState } from "react";
|
import { type ChangeEvent, useMemo, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-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 useSWR from "swr";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
type TCodexAgent,
|
type TCodexAgent,
|
||||||
type TCodexAgentGrant,
|
type TCodexAgentGrant,
|
||||||
type TCodexAgentGrantableWorkspace,
|
type TCodexAgentGrantableWorkspace,
|
||||||
|
type TCodexAgentSetupCode,
|
||||||
type TCodexAgentSetupPacket,
|
type TCodexAgentSetupPacket,
|
||||||
type TCodexAgentToken,
|
type TCodexAgentToken,
|
||||||
} from "@/services/workspace-codex-agent.service";
|
} from "@/services/workspace-codex-agent.service";
|
||||||
|
|
@ -59,7 +60,9 @@ type TProps = {
|
||||||
type TAgentSetupCard = {
|
type TAgentSetupCard = {
|
||||||
agent: TCodexAgent;
|
agent: TCodexAgent;
|
||||||
grants: TCodexAgentGrant[];
|
grants: TCodexAgentGrant[];
|
||||||
|
installCommand?: string;
|
||||||
setup?: TCodexAgentSetupPacket;
|
setup?: TCodexAgentSetupPacket;
|
||||||
|
setupCode?: TCodexAgentSetupCode;
|
||||||
tokens: TCodexAgentToken[];
|
tokens: TCodexAgentToken[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -84,6 +87,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
const [createdSetupCards, setCreatedSetupCards] = useState<TAgentSetupCard[]>([]);
|
const [createdSetupCards, setCreatedSetupCards] = useState<TAgentSetupCard[]>([]);
|
||||||
const [revealedTokens, setRevealedTokens] = useState<Record<string, string>>({});
|
const [revealedTokens, setRevealedTokens] = useState<Record<string, string>>({});
|
||||||
const [updatingAgentIds, setUpdatingAgentIds] = useState<Record<string, boolean>>({});
|
const [updatingAgentIds, setUpdatingAgentIds] = useState<Record<string, boolean>>({});
|
||||||
|
const [creatingSetupCodeAgentIds, setCreatingSetupCodeAgentIds] = useState<Record<string, boolean>>({});
|
||||||
const [creatingTokenAgentIds, setCreatingTokenAgentIds] = useState<Record<string, boolean>>({});
|
const [creatingTokenAgentIds, setCreatingTokenAgentIds] = useState<Record<string, boolean>>({});
|
||||||
const [openProjectAccessAgentId, setOpenProjectAccessAgentId] = useState<string | null>(null);
|
const [openProjectAccessAgentId, setOpenProjectAccessAgentId] = useState<string | null>(null);
|
||||||
const [projectGrantDrafts, setProjectGrantDrafts] = useState<Record<string, string[]>>({});
|
const [projectGrantDrafts, setProjectGrantDrafts] = useState<Record<string, string[]>>({});
|
||||||
|
|
@ -221,19 +225,20 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
mode: "voluntary",
|
mode: "voluntary",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const tokenResponse = await codexAgentService.createToken(workspaceSlug, createResponse.agent.id, {
|
const [setupResponse, setupCodeResponse] = await Promise.all([
|
||||||
name: `${displayName} local token`,
|
codexAgentService.getSetup(workspaceSlug, createResponse.agent.id),
|
||||||
});
|
codexAgentService.createSetupCode(workspaceSlug, createResponse.agent.id, {
|
||||||
setRevealedTokens((currentTokens) => ({
|
expires_in_seconds: 600,
|
||||||
...currentTokens,
|
token_name: `${displayName} local token`,
|
||||||
[tokenResponse.token_record.id]: tokenResponse.token,
|
}),
|
||||||
}));
|
]);
|
||||||
setCreatedSetupCards((currentCards) =>
|
setCreatedSetupCards((currentCards) =>
|
||||||
upsertSetupCardToken(
|
upsertSetupCardSetupCode(
|
||||||
currentCards,
|
currentCards,
|
||||||
createResponse.agent,
|
createResponse.agent,
|
||||||
tokenResponse.token_record,
|
setupCodeResponse.setup_code_record,
|
||||||
tokenResponse.setup,
|
setupCodeResponse.install.command,
|
||||||
|
setupResponse.setup,
|
||||||
grantsResponse.grants
|
grantsResponse.grants
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -242,7 +247,8 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Codex agent создан",
|
title: "Codex agent создан",
|
||||||
message: "Полный token показан только в текущем открытии раздела. После перезахода останется masked suffix.",
|
message:
|
||||||
|
"Скопируйте terminal-команду в локальный терминал. Setup-код действует 10 минут и используется один раз.",
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setToast({
|
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) => {
|
const handleCreateToken = async (agent: TCodexAgent) => {
|
||||||
setCreatingTokenAgentIds((current) => ({ ...current, [agent.id]: true }));
|
setCreatingTokenAgentIds((current) => ({ ...current, [agent.id]: true }));
|
||||||
try {
|
try {
|
||||||
|
|
@ -435,7 +475,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
<CapabilityCard
|
<CapabilityCard
|
||||||
icon={KeyRound}
|
icon={KeyRound}
|
||||||
title="Локальный Codex"
|
title="Локальный Codex"
|
||||||
description="Пользовательский Codex подключается по MCP endpoint с agent token; token хранится только на стороне Gateway."
|
description="Пользовательский Codex подключается по MCP endpoint через одноразовый setup-code или legacy agent token."
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -445,7 +485,8 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="text-15 font-semibold text-primary">Создать агента workspace</div>
|
<div className="text-15 font-semibold text-primary">Создать агента workspace</div>
|
||||||
<p className="max-w-3xl text-12 leading-5 text-tertiary">
|
<p className="max-w-3xl text-12 leading-5 text-tertiary">
|
||||||
Задайте имя, выберите project grant и выпустите agent token. Аватар меняется кликом по кругу.
|
Задайте имя, выберите project grant и получите terminal-команду для локального Codex. Аватар
|
||||||
|
меняется кликом по кругу.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -514,11 +555,14 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
activeAgents.map((agent) => {
|
activeAgents.map((agent) => {
|
||||||
const draftName = getAgentDraftName(agentDraftNames, agent);
|
const draftName = getAgentDraftName(agentDraftNames, agent);
|
||||||
const isUpdatingAgent = updatingAgentIds[agent.id] === true;
|
const isUpdatingAgent = updatingAgentIds[agent.id] === true;
|
||||||
|
const isCreatingSetupCode = creatingSetupCodeAgentIds[agent.id] === true;
|
||||||
const isCreatingToken = creatingTokenAgentIds[agent.id] === true;
|
const isCreatingToken = creatingTokenAgentIds[agent.id] === true;
|
||||||
const isAgentDirty = draftName.trim() !== agent.display_name;
|
const isAgentDirty = draftName.trim() !== agent.display_name;
|
||||||
const setupCard = setupCards.find((card) => card.agent.id === agent.id);
|
const setupCard = setupCards.find((card) => card.agent.id === agent.id);
|
||||||
const agentTokens = setupCard?.tokens ?? [];
|
const agentTokens = setupCard?.tokens ?? [];
|
||||||
const agentGrants = setupCard?.grants ?? [];
|
const agentGrants = setupCard?.grants ?? [];
|
||||||
|
const installCommand = setupCard?.installCommand;
|
||||||
|
const setupCode = setupCard?.setupCode;
|
||||||
const currentGrantKeys = getGrantedProjectKeys(agentGrants);
|
const currentGrantKeys = getGrantedProjectKeys(agentGrants);
|
||||||
const draftGrantKeys = projectGrantDrafts[agent.id] ?? currentGrantKeys;
|
const draftGrantKeys = projectGrantDrafts[agent.id] ?? currentGrantKeys;
|
||||||
const isProjectAccessOpen = openProjectAccessAgentId === agent.id;
|
const isProjectAccessOpen = openProjectAccessAgentId === agent.id;
|
||||||
|
|
@ -584,15 +628,6 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
>
|
>
|
||||||
Сохранить
|
Сохранить
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="nodedc-settings-chip h-12"
|
|
||||||
loading={isCreatingToken}
|
|
||||||
onClick={() => void handleCreateToken(agent)}
|
|
||||||
>
|
|
||||||
Новый токен
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -604,8 +639,18 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AgentTerminalSetupPanel
|
||||||
|
command={installCommand}
|
||||||
|
isCreating={isCreatingSetupCode}
|
||||||
|
setupCode={setupCode}
|
||||||
|
onCopyCommand={() => installCommand && void handleCopy(installCommand, "Terminal-команда")}
|
||||||
|
onCreate={() => void handleCreateSetupCode(agent)}
|
||||||
|
/>
|
||||||
|
|
||||||
{areSetupCardsLoading && agentTokens.length === 0 ? (
|
{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 ? (
|
) : agentTokens.length > 0 ? (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{agentTokens.map((token) => {
|
{agentTokens.map((token) => {
|
||||||
|
|
@ -617,7 +662,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
return (
|
return (
|
||||||
<div key={token.id} className="nodedc-settings-field p-4">
|
<div key={token.id} className="nodedc-settings-field p-4">
|
||||||
<div className="mb-2 text-12 font-semibold tracking-wide text-tertiary uppercase">
|
<div className="mb-2 text-12 font-semibold tracking-wide text-tertiary uppercase">
|
||||||
Agent token
|
Legacy agent token
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<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">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="nodedc-settings-field px-4 py-4 text-13 text-secondary">
|
<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">
|
||||||
Токен ещё не выпущен. Нажмите «Новый токен», чтобы получить доступ для локального Codex.
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -704,12 +760,74 @@ type TCodexConnectionGuideProps = {
|
||||||
onCopyConfig: () => void;
|
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) {
|
function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 p-4">
|
<div className="grid gap-4 p-4">
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-14 font-semibold text-primary">Подключение локального Codex</div>
|
<div className="text-14 font-semibold text-primary">Legacy manual setup</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="nodedc-settings-chip inline-flex min-h-11 w-fit items-center justify-center text-12">
|
<span className="nodedc-settings-chip inline-flex min-h-11 w-fit items-center justify-center text-12">
|
||||||
MCP endpoint · {props.mcpEndpoint}
|
MCP endpoint · {props.mcpEndpoint}
|
||||||
|
|
@ -763,7 +881,7 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
|
||||||
<p className="mb-3 text-13 leading-5 text-secondary">
|
<p className="mb-3 text-13 leading-5 text-secondary">
|
||||||
Добавьте этот блок в конец существующего <code>config.toml</code>. Не заменяйте файл целиком. Если блок{" "}
|
Добавьте этот блок в конец существующего <code>config.toml</code>. Не заменяйте файл целиком. Если блок{" "}
|
||||||
<code>[mcp_servers.{CODEX_MCP_SERVER_NAME}]</code> уже есть — замените только этот блок и его{" "}
|
<code>[mcp_servers.{CODEX_MCP_SERVER_NAME}]</code> уже есть — замените только этот блок и его{" "}
|
||||||
<code>headers</code>.
|
<code>http_headers</code>.
|
||||||
</p>
|
</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">
|
<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}
|
{props.configSnippet}
|
||||||
|
|
@ -790,7 +908,7 @@ required = false
|
||||||
startup_timeout_sec = 20
|
startup_timeout_sec = 20
|
||||||
tool_timeout_sec = 60
|
tool_timeout_sec = 60
|
||||||
|
|
||||||
[mcp_servers.${CODEX_MCP_SERVER_NAME}.headers]
|
[mcp_servers.${CODEX_MCP_SERVER_NAME}.http_headers]
|
||||||
Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
|
Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
|
||||||
Accept = "application/json"
|
Accept = "application/json"
|
||||||
"MCP-Protocol-Version" = "2025-06-18"`;
|
"MCP-Protocol-Version" = "2025-06-18"`;
|
||||||
|
|
@ -994,12 +1112,14 @@ function mergeSetupCards(persistedCards: TAgentSetupCard[], createdCards: TAgent
|
||||||
cardsByAgentId.set(card.agent.id, {
|
cardsByAgentId.set(card.agent.id, {
|
||||||
agent: persistedCard.agent,
|
agent: persistedCard.agent,
|
||||||
grants: mergeAgentGrants(persistedCard.grants, card.grants),
|
grants: mergeAgentGrants(persistedCard.grants, card.grants),
|
||||||
|
installCommand: card.installCommand ?? persistedCard.installCommand,
|
||||||
setup: persistedCard.setup ?? card.setup,
|
setup: persistedCard.setup ?? card.setup,
|
||||||
|
setupCode: card.setupCode ?? persistedCard.setupCode,
|
||||||
tokens: mergeTokens(persistedCard.tokens, card.tokens),
|
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[] {
|
function mergeTokens(primaryTokens: TCodexAgentToken[], secondaryTokens: TCodexAgentToken[]): TCodexAgentToken[] {
|
||||||
|
|
@ -1029,13 +1149,42 @@ function upsertSetupCardToken(
|
||||||
? {
|
? {
|
||||||
agent,
|
agent,
|
||||||
grants: mergeAgentGrants(card.grants, grants),
|
grants: mergeAgentGrants(card.grants, grants),
|
||||||
|
installCommand: card.installCommand,
|
||||||
setup: setup ?? card.setup,
|
setup: setup ?? card.setup,
|
||||||
|
setupCode: card.setupCode,
|
||||||
tokens: mergeTokens([token], card.tokens),
|
tokens: mergeTokens([token], card.tokens),
|
||||||
}
|
}
|
||||||
: card
|
: 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[] {
|
function flattenProjectAccessOptions(workspaceGroups: TCodexAgentGrantableWorkspace[]): TProjectAccessOption[] {
|
||||||
return workspaceGroups.flatMap((workspaceGroup) =>
|
return workspaceGroups.flatMap((workspaceGroup) =>
|
||||||
workspaceGroup.projects.map((project) => ({
|
workspaceGroup.projects.map((project) => ({
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { APIService } from "@/services/api.service";
|
||||||
export type TCodexAgentStatus = "active" | "disabled" | "revoked";
|
export type TCodexAgentStatus = "active" | "disabled" | "revoked";
|
||||||
export type TCodexAgentGrantMode = "voluntary" | "reporting";
|
export type TCodexAgentGrantMode = "voluntary" | "reporting";
|
||||||
export type TCodexAgentTokenStatus = "active" | "revoked" | "expired";
|
export type TCodexAgentTokenStatus = "active" | "revoked" | "expired";
|
||||||
|
export type TCodexAgentSetupCodeStatus = "active" | "used" | "expired" | "revoked";
|
||||||
|
|
||||||
export type TCodexAgent = {
|
export type TCodexAgent = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -45,6 +46,19 @@ export type TCodexAgentToken = {
|
||||||
created_at: string;
|
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 = {
|
export type TCodexAgentSetupPacket = {
|
||||||
agents_md?: string;
|
agents_md?: string;
|
||||||
codex_config_template?: Record<string, unknown>;
|
codex_config_template?: Record<string, unknown>;
|
||||||
|
|
@ -67,6 +81,25 @@ export type TCodexAgentCreateTokenResponse = {
|
||||||
token_record: TCodexAgentToken;
|
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 = {
|
export type TCodexAgentTokenListResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
tokens: TCodexAgentToken[];
|
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> {
|
async listTokens(workspaceSlug: string, agentId: string): Promise<TCodexAgentTokenListResponse> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/codex-agent-api/agents/${agentId}/tokens/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/codex-agent-api/agents/${agentId}/tokens/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue