Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/core/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ import type {
PersonalAccessToken,
CreatePersonalAccessTokenRequest,
CreatePersonalAccessTokenResponse,
CompanyCodexKeyStatus,
CreateCompanyCodexKeyResponse,
CompanyCodexSession,
CompanyCodexSessionDetail,
RuntimeUsage,
IssueUsageSummary,
RuntimeHourlyActivity,
Expand Down Expand Up @@ -351,6 +355,11 @@ import {
type IssueView,
type IssueViewPreference,
type CreateIssueViewRequest,
CompanyCodexKeyStatusSchema,
CreateCompanyCodexKeyResponseSchema,
CompanyCodexSessionListSchema,
CompanyCodexSessionDetailSchema,
EMPTY_COMPANY_CODEX_KEY_STATUS,
} from "./schemas";

/** Identifies the calling client to the server.
Expand Down Expand Up @@ -2335,6 +2344,48 @@ export class ApiClient {
await this.fetch(`/api/tokens/${id}`, { method: "DELETE" });
}

async getCompanyCodexKey(): Promise<CompanyCodexKeyStatus> {
const raw = await this.fetch<unknown>("/api/company-codex/key");
return parseWithFallback(raw, CompanyCodexKeyStatusSchema, EMPTY_COMPANY_CODEX_KEY_STATUS, {
endpoint: "GET /api/company-codex/key",
});
}

async createCompanyCodexKey(): Promise<CreateCompanyCodexKeyResponse> {
const raw = await this.fetch<unknown>("/api/company-codex/key", { method: "POST" });
const created = parseWithFallback<CreateCompanyCodexKeyResponse | null>(
raw,
CreateCompanyCodexKeyResponseSchema,
null,
{ endpoint: "POST /api/company-codex/key" },
);
if (!created) throw new Error();
return created;
}

async revokeCompanyCodexKey(): Promise<void> {
await this.fetch("/api/company-codex/key", { method: "DELETE" });
}

async listCompanyCodexSessions(): Promise<CompanyCodexSession[]> {
const raw = await this.fetch<unknown>("/api/company-codex/sessions");
return parseWithFallback(raw, CompanyCodexSessionListSchema, [], {
endpoint: "GET /api/company-codex/sessions",
});
}

async getCompanyCodexSession(id: string): Promise<CompanyCodexSessionDetail> {
const raw = await this.fetch<unknown>(`/api/company-codex/sessions/${id}`);
const detail = parseWithFallback<CompanyCodexSessionDetail | null>(
raw,
CompanyCodexSessionDetailSchema,
null,
{ endpoint: "GET /api/company-codex/sessions/{id}" },
);
if (!detail) throw new Error();
return detail;
}

// File Upload & Attachments
async uploadFile(
file: File,
Expand Down
41 changes: 41 additions & 0 deletions packages/core/api/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, expect, it } from "vitest";
import {
AppConfigSchema,
CompanyCodexKeyStatusSchema,
CompanyCodexSessionDetailSchema,
CompanyCodexSessionListSchema,
CreateCompanyCodexKeyResponseSchema,
EMPTY_COMPANY_CODEX_KEY_STATUS,
WecomInstallationSchema,
ListWecomInstallationsResponseSchema,
RedeemWecomBindingTokenResponseSchema,
Expand Down Expand Up @@ -51,6 +56,42 @@ import {
import { IssueViewSchema, IssueViewListSchema } from "./schemas";
import { parseWithFallback } from "./schema";

describe("company Codex schemas", () => {
const session = {
id: "session-1",
user_name: "Employee",
user_email: "employee@chekkk.com",
client_session_id: "desktop-session",
title: "Company work",
status: "completed",
input_tokens: 10,
output_tokens: 20,
cached_input_tokens: 0,
started_at: "2026-08-18T00:00:00Z",
last_activity_at: "2026-08-18T00:01:00Z",
};

it("defaults malformed key status to inactive", () => {
expect(parseWithFallback(null, CompanyCodexKeyStatusSchema, EMPTY_COMPANY_CODEX_KEY_STATUS, {
endpoint: "GET /api/company-codex/key",
})).toEqual({ active: false });
});

it("rejects an issued key response without its one-time credential", () => {
expect(CreateCompanyCodexKeyResponseSchema.safeParse({ active: true }).success).toBe(false);
});

it("keeps future status values and rejects malformed session list items", () => {
expect(CompanyCodexSessionListSchema.parse([{ ...session, status: "archived" }])[0]?.status)
.toBe("archived");
expect(CompanyCodexSessionListSchema.safeParse([{ title: "missing id" }]).success).toBe(false);
});

it("defaults a missing turn list in session detail", () => {
expect(CompanyCodexSessionDetailSchema.parse({ session }).turns).toEqual([]);
});
});

const baseIssue = {
id: "11111111-1111-1111-1111-111111111111",
workspace_id: "ws-1",
Expand Down
57 changes: 57 additions & 0 deletions packages/core/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
BillingTopupsPage,
BillingTransactionsPage,
CancelTaskResponse,
CompanyCodexKeyStatus,
ChatMessage,
ChatDraftRestoresResponse,
ChatPendingTask,
Expand Down Expand Up @@ -65,6 +66,62 @@ import type {
import type { CloudRuntimeNode } from "../runtimes/cloud-runtime";
import type { CreateFeedbackResponse } from "../feedback/types";

export const CompanyCodexKeyStatusSchema = z.object({
active: z.boolean().default(false),
key_prefix: z.string().optional(),
created_at: z.string().optional(),
}).loose();

export const EMPTY_COMPANY_CODEX_KEY_STATUS: CompanyCodexKeyStatus = {
active: false,
};

export const CreateCompanyCodexKeyResponseSchema = CompanyCodexKeyStatusSchema.extend({
credential: z.string().min(1),
cc_switch_url: z.string().min(1),
config_toml: z.string().min(1),
setup_command: z.string().min(1),
}).loose();

export const CompanyCodexSessionSchema = z.object({
id: z.string().min(1),
user_name: z.string().default(""),
user_email: z.string().default(""),
client_session_id: z.string().default(""),
client_thread_id: z.string().optional(),
title: z.string().default("Codex GUI session"),
model: z.string().optional(),
status: z.string().default("completed"),
input_tokens: z.number().default(0),
output_tokens: z.number().default(0),
cached_input_tokens: z.number().default(0),
started_at: z.string().default(""),
last_activity_at: z.string().default(""),
last_prompt: z.string().optional(),
last_response: z.string().optional(),
}).loose();

export const CompanyCodexSessionListSchema = z.array(CompanyCodexSessionSchema);

export const CompanyCodexTurnSchema = z.object({
id: z.string().min(1),
request_id: z.string().default(""),
prompt: z.string().default(""),
response: z.string().default(""),
model: z.string().optional(),
status: z.string().default("completed"),
input_tokens: z.number().default(0),
output_tokens: z.number().default(0),
cached_input_tokens: z.number().default(0),
started_at: z.string().default(""),
completed_at: z.string().default(""),
}).loose();

export const CompanyCodexSessionDetailSchema = z.object({
session: CompanyCodexSessionSchema,
turns: z.array(CompanyCodexTurnSchema).default([]),
}).loose();

export const GitHubInstallationSchema = z.object({
id: z.string(),
workspace_id: z.string(),
Expand Down
50 changes: 50 additions & 0 deletions packages/core/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,56 @@ export interface CreatePersonalAccessTokenResponse extends PersonalAccessToken {
token: string;
}

export interface CompanyCodexKeyStatus {
active: boolean;
key_prefix?: string;
created_at?: string;
}

export interface CreateCompanyCodexKeyResponse extends CompanyCodexKeyStatus {
credential: string;
cc_switch_url: string;
config_toml: string;
setup_command: string;
}

export interface CompanyCodexSession {
id: string;
user_name: string;
user_email: string;
client_session_id: string;
client_thread_id?: string;
title: string;
model?: string;
status: "running" | "completed" | "failed";
input_tokens: number;
output_tokens: number;
cached_input_tokens: number;
started_at: string;
last_activity_at: string;
last_prompt?: string;
last_response?: string;
}

export interface CompanyCodexTurn {
id: string;
request_id: string;
prompt: string;
response: string;
model?: string;
status: "completed" | "failed";
input_tokens: number;
output_tokens: number;
cached_input_tokens: number;
started_at: string;
completed_at: string;
}

export interface CompanyCodexSessionDetail {
session: CompanyCodexSession;
turns: CompanyCodexTurn[];
}

// Pagination
export interface PaginationParams {
limit?: number;
Expand Down
40 changes: 40 additions & 0 deletions packages/views/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,46 @@
"checking": "Checking…"
}
},
"company_codex": {
"title": "Company Codex access",
"description": "Use the official Codex app through CHEK's managed account pool. The credential has no fixed expiry, but remains valid only while you are an active workspace member.",
"audit_notice": "Company Codex conversations are synchronized to Multica with prompts, responses, model, token usage, and timestamps. Use this access only for company work.",
"active": "Managed credential",
"inactive": "No managed credential",
"enabled": "Active",
"not_enabled": "Not configured",
"key_metadata": "{{prefix}} · issued {{date}}",
"create": "Create access",
"rotate": "Rotate key",
"revoke": "Revoke",
"created": "Company Codex access created",
"revoked": "Company Codex access revoked",
"load_failed": "Failed to load company Codex access",
"create_failed": "Failed to create company Codex access",
"revoke_failed": "Failed to revoke company Codex access",
"sessions_title": "Codex GUI sessions",
"sessions_description": "Your recent official Codex GUI windows. Workspace owners and admins can review all member sessions.",
"sessions_empty": "No synchronized Codex GUI sessions yet.",
"session_load_failed": "Failed to load Codex session",
"refresh": "Refresh",
"unknown_model": "Unknown model",
"created_title": "Configure official Codex GUI",
"created_warning": "This credential is shown once. Rotating or revoking it immediately invalidates the previous local configuration.",
"credential": "Managed API credential",
"cc_switch_hint": "Recommended: import and switch the provider with one confirmation.",
"open_cc_switch": "Open in CC Switch",
"official_gui": "Official Codex GUI",
"official_gui_hint": "Run the one-time poolctl command, then paste the credential when prompted. No custom launcher is installed.",
"copy_command": "Copy setup command",
"manual_config": "Manual config.toml fallback",
"copy_config": "Copy configuration",
"done": "Done",
"loading_session": "Loading Codex session",
"employee_prompt": "Employee prompt",
"codex_response": "Codex response",
"cc_switch": "CC Switch",
"token_count": "{{count}} tokens"
},
"members": {
"section_title": "Members ({{count}})",
"invite_title": "Invite member",
Expand Down
40 changes: 40 additions & 0 deletions packages/views/locales/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,46 @@
"checking": "確認中…"
}
},
"company_codex": {
"title": "会社 Codex アクセス",
"description": "公式 Codex アプリから CHEK の管理対象アカウントプールを利用します。認証情報に固定の有効期限はありませんが、ワークスペースの有効なメンバーである間のみ利用できます。",
"audit_notice": "会社 Codex の会話は、プロンプト、回答、モデル、Token 使用量、時刻とともに Multica に同期されます。会社業務にのみ使用してください。",
"active": "管理対象認証情報",
"inactive": "管理対象認証情報なし",
"enabled": "有効",
"not_enabled": "未設定",
"key_metadata": "{{prefix}} · {{date}} に発行",
"create": "アクセスを作成",
"rotate": "キーをローテーション",
"revoke": "無効化",
"created": "会社 Codex アクセスを作成しました",
"revoked": "会社 Codex アクセスを無効化しました",
"load_failed": "会社 Codex アクセスを読み込めませんでした",
"create_failed": "会社 Codex アクセスを作成できませんでした",
"revoke_failed": "会社 Codex アクセスを無効化できませんでした",
"sessions_title": "Codex GUI セッション",
"sessions_description": "公式 Codex GUI の最近のウィンドウです。ワークスペースのオーナーと管理者は全メンバーのセッションを確認できます。",
"sessions_empty": "同期された Codex GUI セッションはまだありません。",
"session_load_failed": "Codex セッションを読み込めませんでした",
"refresh": "更新",
"unknown_model": "不明なモデル",
"created_title": "公式 Codex GUI を設定",
"created_warning": "この認証情報は一度だけ表示されます。ローテーションまたは無効化すると、以前のローカル設定は直ちに無効になります。",
"credential": "管理対象 API 認証情報",
"cc_switch_hint": "推奨: 1 回の確認でプロバイダーをインポートして切り替えます。",
"open_cc_switch": "CC Switch で開く",
"official_gui": "公式 Codex GUI",
"official_gui_hint": "poolctl コマンドを一度実行し、表示されたプロンプトに認証情報を貼り付けます。専用ランチャーはインストールされません。",
"copy_command": "設定コマンドをコピー",
"manual_config": "手動 config.toml 予備設定",
"copy_config": "設定をコピー",
"done": "完了",
"loading_session": "Codex セッションを読み込み中",
"employee_prompt": "従業員のプロンプト",
"codex_response": "Codex の回答",
"cc_switch": "CC Switch",
"token_count": "{{count}} tokens"
},
"members": {
"section_title": "メンバー({{count}})",
"invite_title": "メンバーを招待",
Expand Down
40 changes: 40 additions & 0 deletions packages/views/locales/ko/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,46 @@
"checking": "확인 중..."
}
},
"company_codex": {
"title": "회사 Codex 액세스",
"description": "공식 Codex 앱에서 CHEK 관리 계정 풀을 사용합니다. 자격 증명에는 고정 만료 시간이 없지만 활성 워크스페이스 멤버인 동안에만 유효합니다.",
"audit_notice": "회사 Codex 대화는 프롬프트, 응답, 모델, Token 사용량 및 시간과 함께 Multica에 동기화됩니다. 회사 업무에만 사용하세요.",
"active": "관리형 자격 증명",
"inactive": "관리형 자격 증명 없음",
"enabled": "활성",
"not_enabled": "미설정",
"key_metadata": "{{prefix}} · {{date}} 발급",
"create": "액세스 생성",
"rotate": "키 교체",
"revoke": "해지",
"created": "회사 Codex 액세스를 생성했습니다",
"revoked": "회사 Codex 액세스를 해지했습니다",
"load_failed": "회사 Codex 액세스를 불러오지 못했습니다",
"create_failed": "회사 Codex 액세스를 생성하지 못했습니다",
"revoke_failed": "회사 Codex 액세스를 해지하지 못했습니다",
"sessions_title": "Codex GUI 세션",
"sessions_description": "공식 Codex GUI의 최근 창입니다. 워크스페이스 소유자와 관리자는 모든 멤버의 세션을 검토할 수 있습니다.",
"sessions_empty": "동기화된 Codex GUI 세션이 아직 없습니다.",
"session_load_failed": "Codex 세션을 불러오지 못했습니다",
"refresh": "새로고침",
"unknown_model": "알 수 없는 모델",
"created_title": "공식 Codex GUI 설정",
"created_warning": "이 자격 증명은 한 번만 표시됩니다. 교체하거나 해지하면 이전 로컬 설정은 즉시 무효화됩니다.",
"credential": "관리형 API 자격 증명",
"cc_switch_hint": "권장: 한 번의 확인으로 공급자를 가져오고 전환합니다.",
"open_cc_switch": "CC Switch에서 열기",
"official_gui": "공식 Codex GUI",
"official_gui_hint": "poolctl 명령을 한 번 실행한 뒤 프롬프트에 자격 증명을 붙여 넣으세요. 전용 실행기는 설치되지 않습니다.",
"copy_command": "설정 명령 복사",
"manual_config": "수동 config.toml 대체 설정",
"copy_config": "설정 복사",
"done": "완료",
"loading_session": "Codex 세션 불러오는 중",
"employee_prompt": "직원 프롬프트",
"codex_response": "Codex 응답",
"cc_switch": "CC Switch",
"token_count": "{{count}} tokens"
},
"members": {
"section_title": "멤버 ({{count}})",
"invite_title": "멤버 초대",
Expand Down
Loading
Loading