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
1 change: 1 addition & 0 deletions apps/landing/src/shared/icons/brands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ const BRAND_ICONS = {
bigquery: createSimpleBrandIcon(siGooglebigquery),
bun: createSimpleBrandIcon(siBun),
cal: createSimpleBrandIcon(siCaldotcom),
codex_app_server_api: HermesIcon,
cloudflare_d1: CloudflareD1Icon,
cloudflare_r2_sql: createSimpleBrandIcon(siCloudflareworkers),
cloudflare_web_analytics: createSimpleBrandIcon(siCloudflareworkers),
Expand Down
1 change: 1 addition & 0 deletions packages/db/src/__snapshots__/credentials.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports[`credentials schemas > credentialSchemaMap > matches supported provider
"cloudflare_r2_sql",
"cloudflare_web_analytics",
"cloudflare_workers_observability",
"codex_app_server_api",
"confluence",
"discord",
"e2b",
Expand Down
48 changes: 48 additions & 0 deletions packages/db/src/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
AirtableCredentialsSchema,
BigQueryCredentialsSchema,
CalCredentialsSchema,
CodexAppServerApiCredentialsSchema,
CloudflareD1CredentialsSchema,
CloudflareR2SqlCredentialsSchema,
CloudflareWebAnalyticsCredentialsSchema,
Expand Down Expand Up @@ -57,6 +58,7 @@ import type {
AirtableCredentials,
BigQueryCredentials,
CalCredentials,
CodexAppServerApiCredentials,
CloudflareD1Credentials,
CloudflareR2SqlCredentials,
CloudflareWebAnalyticsCredentials,
Expand Down Expand Up @@ -1846,6 +1848,46 @@ describe("credentials schemas", () => {
});
});

describe("CodexAppServerApiCredentialsSchema", () => {
it("validates Codex App Server API credentials", () => {
const credentials: CodexAppServerApiCredentials = {
apiBaseUrl: "https://codex-app-server-api.example.com",
apiKey: "sk-codex-app-server-key",
type: "codex_app_server_api",
};

const result = CodexAppServerApiCredentialsSchema.safeParse(credentials);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(credentials);
}
});

it("trims apiBaseUrl and removes its trailing slash", () => {
const result = CodexAppServerApiCredentialsSchema.safeParse({
apiBaseUrl: " https://codex-app-server-api.example.com/ ",
apiKey: "sk-codex-app-server-key",
type: "codex_app_server_api",
});

expect(result.success).toBe(true);
if (result.success) {
expect(result.data.apiBaseUrl).toBe(
"https://codex-app-server-api.example.com"
);
}
});

it("rejects missing API key", () => {
const result = CodexAppServerApiCredentialsSchema.safeParse({
apiBaseUrl: "https://codex-app-server-api.example.com",
type: "codex_app_server_api",
});

expect(result.success).toBe(false);
});
});

describe("HermesCredentialsSchema", () => {
it("validates Hermes credentials", () => {
const credentials: HermesCredentials = {
Expand Down Expand Up @@ -2168,6 +2210,12 @@ describe("credentials schemas", () => {
expect(credentialSchemaMap.e2b).toBe(E2BCredentialsSchema);
});

it("should map codex_app_server_api to CodexAppServerApiCredentialsSchema", () => {
expect(credentialSchemaMap.codex_app_server_api).toBe(
CodexAppServerApiCredentialsSchema
);
});

it("should map hermes to HermesCredentialsSchema", () => {
expect(credentialSchemaMap.hermes).toBe(HermesCredentialsSchema);
});
Expand Down
15 changes: 15 additions & 0 deletions packages/db/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,19 @@ export const E2BCredentialsSchema = z.object({

export type E2BCredentials = z.infer<typeof E2BCredentialsSchema>;

export const CodexAppServerApiCredentialsSchema = z.object({
apiBaseUrl: trimmedUrl(
"API base URL is required",
"API base URL must be a valid URL"
).transform((value) => value.replace(/\/+$/, "")),
apiKey: requiredOpaqueString("API key is required"),
type: z.literal("codex_app_server_api"),
});

export type CodexAppServerApiCredentials = z.infer<
typeof CodexAppServerApiCredentialsSchema
>;

export const HermesCredentialsSchema = z.object({
apiBaseUrl: trimmedUrl(
"API base URL is required",
Expand Down Expand Up @@ -664,6 +677,7 @@ export const CredentialsSchema = z.union([
JiraCredentialsSchema,
VercelCredentialsSchema,
E2BCredentialsSchema,
CodexAppServerApiCredentialsSchema,
HermesCredentialsSchema,
MicrosoftClarityCredentialsSchema,
OnePasswordCredentialsSchema,
Expand Down Expand Up @@ -717,6 +731,7 @@ export const credentialSchemaMap = {
aws_athena_connector: ConnectorCredentialsSchema,
bigquery: BigQueryCredentialsSchema,
cal: CalCredentialsSchema,
codex_app_server_api: CodexAppServerApiCredentialsSchema,
cloudflare_d1: CloudflareD1CredentialsSchema,
cloudflare_r2_sql: CloudflareR2SqlCredentialsSchema,
confluence: ConfluenceCredentialsSchema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports[`data-sources schema > matches provider type snapshots 1`] = `
"snowflake",
"mongodb",
"bigquery",
"codex_app_server_api",
"cloudflare_d1",
"cloudflare_r2_sql",
"laminar",
Expand Down Expand Up @@ -49,6 +50,7 @@ exports[`data-sources schema > matches provider type snapshots 1`] = `
"snowflake",
"mongodb",
"bigquery",
"codex_app_server_api",
"cloudflare_d1",
"cloudflare_r2_sql",
"laminar",
Expand Down
32 changes: 32 additions & 0 deletions packages/db/src/source-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
AirtableCredentialsSchema,
BigQueryCredentialsSchema,
CalCredentialsSchema,
CodexAppServerApiCredentialsSchema,
CloudflareD1CredentialsSchema,
CloudflareR2SqlCredentialsSchema,
CloudflareWebAnalyticsCredentialsSchema,
Expand Down Expand Up @@ -292,6 +293,37 @@ export const SOURCE_PROVIDER_REGISTRY = {
},
},
},
codex_app_server_api: {
label: "Codex App Server API",
credentialSchema: CodexAppServerApiCredentialsSchema,
credentialType: "codex_app_server_api",
connectable: true,
analysisSource: true,
queryInterface: false,
sourceApiInterface: true,
testable: false,
dashboardConnectable: true,
dashboardCredentialForm: "json",
publicCategory: "Developer workflow",
guide: {
summary:
"Connect the OpenAI-compatible Codex App Server API without exposing its API key to the caller.",
steps: [
"Run the local codex-app-server-api origin and expose it through a secure HTTPS tunnel or private network endpoint reachable from OneQuery.",
"Set `apiBaseUrl` to the API origin, for example `https://codex-app-server-api.example.com`.",
"Create or copy a codex-app-server-api key and store it in `apiKey`.",
"Call native paths such as `/v1/models`, `/v1/responses`, and `/v1/chat/completions` through the Source API.",
],
exampleInput: {
sourceKey: "codex_app_server",
credentials: {
type: "codex_app_server_api",
apiBaseUrl: "https://codex-app-server-api.example.com",
apiKey: "sk-codex-app-server-key",
},
},
},
},
cloudflare_d1: {
label: "Cloudflare D1",
credentialSchema: CloudflareD1CredentialsSchema,
Expand Down
188 changes: 188 additions & 0 deletions packages/server/src/source-api/adapters/codex-app-server-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { afterEach, describe, expect, it, vi } from "vitest";

import type { PreparedSourceConnection, SourceApiActorContext } from "../types";
import { codexAppServerApiSourceApiAdapter } from "./codex-app-server-api";

const originalFetch = globalThis.fetch;

const actor: SourceApiActorContext = {
capabilities: ["source_api.execute"],
membershipRoles: ["owner"],
organizationId: "org_1",
organizationSlug: "acme",
userId: "user_1",
};

const source: PreparedSourceConnection = {
credentials: {
apiBaseUrl: "https://codex-app-server-api.example.com",
apiKey: "sk-codex-secret",
type: "codex_app_server_api",
},
displayName: "Codex App Server API",
id: "source_1",
provider: "codex_app_server_api",
sourceKey: "codex-main",
};

afterEach(() => {
globalThis.fetch = originalFetch;
vi.restoreAllMocks();
});

describe("codexAppServerApiSourceApiAdapter", () => {
it("describes native Codex App Server API operations", async () => {
const descriptor = await codexAppServerApiSourceApiAdapter.describe({
actor,
source,
});

expect(descriptor).toMatchObject({
defaultPathOperation: "fetch_api",
descriptorVersion: "codex-app-server-api.v1",
operations: [
{
kind: "http_request",
methodPolicy: {
allowedMethods: ["GET", "POST"],
defaultMethod: "GET",
},
name: "fetch_api",
selectorKind: "path",
},
],
source: {
provider: "codex_app_server_api",
sourceKey: "codex-main",
},
});
expect(descriptor.examples.map((example) => example.command)).toEqual(
expect.arrayContaining([
expect.stringContaining("/v1/responses --method POST"),
expect.stringContaining("/v1/chat/completions --method POST"),
])
);
});

it("normalizes workspace headers, bodies, and query params", async () => {
const descriptor = await codexAppServerApiSourceApiAdapter.describe({
actor,
source,
});

const plan = await codexAppServerApiSourceApiAdapter.normalize({
actor,
descriptor,
request: {
body: {
kind: "json",
value: {
input: "Summarize the repository",
model: "gpt-5.4",
},
},
fieldPatch: {
params: {
trace: "full",
},
timeoutMs: 60_000,
},
headers: [
{ name: "Idempotency-Key", value: "idem_123" },
{ name: "X-Workspace-Path", value: "/Users/dev/git/velen" },
],
methodOverride: "POST",
operation: "fetch_api",
selector: "/v1/responses",
},
source,
});

expect(plan).toMatchObject({
descriptorVersion: "codex-app-server-api.v1",
headers: [
{ name: "Idempotency-Key", value: "idem_123" },
{ name: "X-Workspace-Path", value: "/Users/dev/git/velen" },
],
kind: "http_request",
method: "POST",
operation: "fetch_api",
query: {
trace: "full",
},
selector: "/v1/responses",
timeoutMs: 60_000,
url: "https://codex-app-server-api.example.com/v1/responses?trace=full",
});
});

it("executes native Codex requests without rewriting their payload", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ id: "resp_123", output_text: "OK" }), {
headers: {
"content-type": "application/json",
"x-request-id": "req_123",
},
status: 200,
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;

const response = await codexAppServerApiSourceApiAdapter.execute({
actor,
prepared: {
body: {
kind: "json",
value: {
input: "Say OK",
model: "gpt-5.4",
},
},
bodyKind: "json",
bodyPaths: ["input", "model"],
descriptorVersion: "codex-app-server-api.v1",
headerNames: ["X-Workspace-Path"],
headers: [{ name: "X-Workspace-Path", value: "/Users/dev/git/velen" }],
kind: "http_request",
method: "POST",
operation: "fetch_api",
paginationPolicy: "none",
preparedBinding: "binding",
provider: "codex_app_server_api",
selector: "/v1/responses",
selectorTemplate: "/{path}",
sourceId: "source_1",
sourceKey: "codex-main",
url: "https://codex-app-server-api.example.com/v1/responses",
},
source,
});

expect(response).toMatchObject({
body: {
kind: "json",
value: { id: "resp_123", output_text: "OK" },
},
operation: "fetch_api",
selector: "/v1/responses",
status: 200,
});
expect(response.headers).toContainEqual({
name: "x-request-id",
value: "req_123",
});

const [calledUrl, calledInit] = fetchMock.mock.calls[0] ?? [];
expect(String(calledUrl)).toBe(
"https://codex-app-server-api.example.com/v1/responses"
);
expect(calledInit?.headers).toMatchObject({
Authorization: "Bearer sk-codex-secret",
"X-Workspace-Path": "/Users/dev/git/velen",
});
expect(JSON.parse(String(calledInit?.body))).toEqual({
input: "Say OK",
model: "gpt-5.4",
});
});
});
Loading