55c8b461be
* feat: surface agent subscription usage in settings and chat tooltip Adds live subscription rate-limit visibility for Claude Code and Codex: - GET /api/v1/agents/usage lists per-agent utilization windows (plan, percent, reset time) for host agents with subscription credentials; ?fresh=true bypasses the 5-minute cache with a 15s per-provider clamp. - Fixes both existing usage clients: Claude parsed float utilization as int (decode failure) and now reads the richer limits[] array incl. per-model weekly windows; Codex read a nonexistent auth.json field and response headers - now reads tokens.access_token, parses the body windows, and refreshes OAuth tokens on 401. Token refresh persists without dropping unknown credential fields. - Settings > Agents gains a Subscription Usage section; the chat context doughnut tooltip shows the session agent's windows, fetched fresh on hover. - Shared UsageWindowRows component (bars, Good/High/Critical status, reset countdowns) keeps settings, tooltip, and office surfaces consistent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: recompute agent billing type on every read Subscription billing detection for Claude and Codex was cached with sync.OnceValue for the process lifetime, so logging in with OAuth after the backend started never flipped billing badges or office usage checks until restart. Detection now re-reads the credential files on each call and reuses the usage clients' HasSubscriptionCredentials checks. Also adds a regression assertion that fractional-second resets_at timestamps parse under the RFC3339 layout. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address subscription usage review feedback - serialize empty utilization windows as arrays, not null - sanitize per-agent usage errors in the API (raw provider errors are logged server-side; they can carry credential paths/response bodies) - persist refreshed CLI credentials via atomic temp-file rename - keep previous settings usage on transient refresh failure; drop stale tooltip usage (and its memo) when a fresh fetch fails - add controller tests for SubscriptionUsage mapping and fresh propagation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: coalesce concurrent usage fetches and guard codex token refresh - serialize usage-cache fetches per key with a double-checked lock so concurrent fresh requests make one provider call instead of a burst - reject codex refresh responses with an empty access_token instead of persisting them and wiping working credentials - pin the usage-rows reset fixture 30s under the day boundary so the formatted value is deterministic on fast runners Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: coalesce failed usage fetches with short negative caching Concurrent lookups after a fetch error each retried the provider once the per-key lock rotated. Failures are now cached for 15 seconds (never longer than the caller's staleness bound), so bursts share one error result and the provider gets a breather before the next retry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: keep stale usage through failed refreshes and skip caching cancellations Track the last success and last failure independently in the usage cache: a failed fresh refresh no longer evicts a still-valid success (non-fresh callers keep the stale value while fresh callers share the recent error), and a caller-cancelled context no longer records a negative entry that would penalize other callers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: synchronize concurrent usage-cache tests with a channel barrier Replaces the sleep-widened race window in the coalescing tests with a started/release barrier per the backend testing guidelines, and dedupes the two concurrent-caller tests into one helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
669 lines
19 KiB
TypeScript
669 lines
19 KiB
TypeScript
import { fetchJson, fetchJsonWithRetry, type ApiRequestOptions } from "../client";
|
|
import { getBackendConfig } from "@/lib/config";
|
|
import type {
|
|
Agent,
|
|
AgentSubscriptionUsageResponse,
|
|
ListAgentsResponse,
|
|
ListAgentDiscoveryResponse,
|
|
ListAvailableAgentsResponse,
|
|
AgentProfileMcpConfig,
|
|
ListExecutorsResponse,
|
|
ListExecutorProfilesResponse,
|
|
ExecutorProfile,
|
|
ProfileEnvVar,
|
|
NotificationProvidersResponse,
|
|
NotificationProvider,
|
|
EditorsResponse,
|
|
EditorOption,
|
|
CustomPrompt,
|
|
PromptsResponse,
|
|
UserSettingsResponse,
|
|
UserSettingsUpdatePayload,
|
|
DynamicModelsResponse,
|
|
} from "@/lib/types/http";
|
|
import type {
|
|
SystemMetricsGlobalSettings,
|
|
SystemMetricsSettingsResponse,
|
|
} from "@/lib/types/system";
|
|
|
|
// User settings
|
|
export async function fetchUserSettings(options?: ApiRequestOptions) {
|
|
return fetchJsonWithRetry<UserSettingsResponse>("/api/v1/user/settings", options);
|
|
}
|
|
|
|
export async function updateUserSettings(
|
|
payload: UserSettingsUpdatePayload,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<UserSettingsResponse>("/api/v1/user/settings", {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function fetchSystemMetricsSettings(options?: ApiRequestOptions) {
|
|
return fetchJsonWithRetry<SystemMetricsSettingsResponse>(
|
|
"/api/v1/system/metrics/settings",
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function updateSystemMetricsSettings(
|
|
payload: SystemMetricsGlobalSettings,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<SystemMetricsSettingsResponse>("/api/v1/system/metrics/settings", {
|
|
...options,
|
|
init: { ...(options?.init ?? {}), method: "PATCH", body: JSON.stringify(payload) },
|
|
});
|
|
}
|
|
|
|
// Executors
|
|
export async function listExecutors(options?: ApiRequestOptions): Promise<ListExecutorsResponse> {
|
|
return fetchJson<ListExecutorsResponse>("/api/v1/executors", options);
|
|
}
|
|
|
|
export async function fetchExecutor(
|
|
executorId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ id: string; name: string; type: string; config?: Record<string, string> }> {
|
|
return fetchJson<{ id: string; name: string; type: string; config?: Record<string, string> }>(
|
|
`/api/v1/executors/${executorId}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function createExecutor(
|
|
payload: {
|
|
name: string;
|
|
type: string;
|
|
config?: Record<string, string>;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ id: string; name: string; type: string; config?: Record<string, string> }> {
|
|
return fetchJson<{ id: string; name: string; type: string; config?: Record<string, string> }>(
|
|
"/api/v1/executors",
|
|
{
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function updateExecutor(
|
|
executorId: string,
|
|
payload: { name?: string; config?: Record<string, string> },
|
|
options?: ApiRequestOptions,
|
|
): Promise<void> {
|
|
await fetchJson<void>(`/api/v1/executors/${executorId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Executor profiles
|
|
export async function listExecutorProfiles(
|
|
executorId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListExecutorProfilesResponse> {
|
|
return fetchJson<ListExecutorProfilesResponse>(
|
|
`/api/v1/executors/${executorId}/profiles`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function listAllExecutorProfiles(
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListExecutorProfilesResponse> {
|
|
return fetchJson<ListExecutorProfilesResponse>("/api/v1/executor-profiles", options);
|
|
}
|
|
|
|
export type ScriptPlaceholder = {
|
|
key: string;
|
|
description: string;
|
|
example: string;
|
|
executor_types: string[];
|
|
};
|
|
|
|
export async function listScriptPlaceholders(
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ placeholders: ScriptPlaceholder[] }> {
|
|
return fetchJson<{ placeholders: ScriptPlaceholder[] }>("/api/v1/script-placeholders", options);
|
|
}
|
|
|
|
export async function fetchDefaultScripts(
|
|
executorType: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ prepare_script: string; cleanup_script: string }> {
|
|
return fetchJson<{ prepare_script: string; cleanup_script: string }>(
|
|
`/api/v1/executor-profiles/default-script?type=${encodeURIComponent(executorType)}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function createExecutorProfile(
|
|
executorId: string,
|
|
payload: {
|
|
name: string;
|
|
mcp_policy?: string;
|
|
config?: Record<string, string>;
|
|
prepare_script?: string;
|
|
cleanup_script?: string;
|
|
env_vars?: ProfileEnvVar[];
|
|
},
|
|
options?: ApiRequestOptions,
|
|
): Promise<ExecutorProfile> {
|
|
return fetchJson<ExecutorProfile>(`/api/v1/executors/${executorId}/profiles`, {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function updateExecutorProfile(
|
|
executorId: string,
|
|
profileId: string,
|
|
payload: {
|
|
name?: string;
|
|
mcp_policy?: string;
|
|
config?: Record<string, string>;
|
|
prepare_script?: string;
|
|
cleanup_script?: string;
|
|
env_vars?: ProfileEnvVar[];
|
|
},
|
|
options?: ApiRequestOptions,
|
|
): Promise<ExecutorProfile> {
|
|
return fetchJson<ExecutorProfile>(`/api/v1/executors/${executorId}/profiles/${profileId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function deleteExecutorProfile(
|
|
executorId: string,
|
|
profileId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ success: boolean }> {
|
|
return fetchJson<{ success: boolean }>(`/api/v1/executors/${executorId}/profiles/${profileId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Agents
|
|
import { normalizeAgentProfile } from "@/lib/api/domains/agent-profile-normalize";
|
|
|
|
function normalizeAgentResponse(agent: Agent): Agent {
|
|
return {
|
|
...agent,
|
|
profiles: (agent.profiles ?? []).map((profile) => normalizeAgentProfile(profile)),
|
|
};
|
|
}
|
|
|
|
export async function listAgents(options?: ApiRequestOptions): Promise<ListAgentsResponse> {
|
|
const res = await fetchJson<ListAgentsResponse>("/api/v1/agents", options);
|
|
return { ...res, agents: (res.agents ?? []).map(normalizeAgentResponse) };
|
|
}
|
|
|
|
export async function listAgentDiscovery(
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListAgentDiscoveryResponse> {
|
|
return fetchJson<ListAgentDiscoveryResponse>("/api/v1/agents/discovery", options);
|
|
}
|
|
|
|
export async function listAvailableAgents(
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListAvailableAgentsResponse> {
|
|
return fetchJson<ListAvailableAgentsResponse>("/api/v1/agents/available", options);
|
|
}
|
|
|
|
export async function listAgentSubscriptionUsage(
|
|
options?: ApiRequestOptions & { fresh?: boolean },
|
|
): Promise<AgentSubscriptionUsageResponse> {
|
|
const { fresh, ...rest } = options ?? {};
|
|
const path = fresh ? "/api/v1/agents/usage?fresh=true" : "/api/v1/agents/usage";
|
|
return fetchJson<AgentSubscriptionUsageResponse>(path, rest);
|
|
}
|
|
|
|
export async function getAgentProfileMcpConfig(
|
|
profileId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<AgentProfileMcpConfig> {
|
|
return fetchJson<AgentProfileMcpConfig>(
|
|
`/api/v1/agent-profiles/${profileId}/mcp-config`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export type CommandPreviewRequest = {
|
|
model: string;
|
|
permission_settings: Record<string, boolean>;
|
|
cli_passthrough: boolean;
|
|
};
|
|
|
|
export type CommandPreviewResponse = {
|
|
supported: boolean;
|
|
command: string[];
|
|
command_string: string;
|
|
};
|
|
|
|
export async function previewAgentCommand(
|
|
agentName: string,
|
|
payload: CommandPreviewRequest,
|
|
options?: ApiRequestOptions,
|
|
): Promise<CommandPreviewResponse> {
|
|
return fetchJson<CommandPreviewResponse>(`/api/v1/agent-command-preview/${agentName}`, {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export type InstallJobStatus = "queued" | "running" | "succeeded" | "failed";
|
|
|
|
export type InstallJob = {
|
|
job_id: string;
|
|
agent_name: string;
|
|
status: InstallJobStatus;
|
|
output?: string;
|
|
error?: string;
|
|
exit_code?: number;
|
|
started_at: string;
|
|
finished_at?: string;
|
|
};
|
|
|
|
/** Enqueue an install. Returns the job snapshot (status=queued or running). */
|
|
export async function installAgent(
|
|
agentName: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<InstallJob> {
|
|
return fetchJson<InstallJob>(`/api/v1/agent-install/${agentName}`, {
|
|
...options,
|
|
init: { method: "POST", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function listInstallJobs(
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ jobs: InstallJob[] }> {
|
|
return fetchJson<{ jobs: InstallJob[] }>("/api/v1/agent-install/jobs", options);
|
|
}
|
|
|
|
export async function getInstallJob(
|
|
jobId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<InstallJob> {
|
|
return fetchJson<InstallJob>(`/api/v1/agent-install/jobs/${jobId}`, options);
|
|
}
|
|
|
|
export type AgentLoginSession = {
|
|
session_id: string;
|
|
agent_id: string;
|
|
cmd: string[];
|
|
running: boolean;
|
|
started_at: string;
|
|
finished_at?: string;
|
|
exit_code?: number;
|
|
};
|
|
|
|
export async function startAgentLogin(
|
|
agentName: string,
|
|
size: { cols: number; rows: number },
|
|
options?: ApiRequestOptions,
|
|
): Promise<AgentLoginSession> {
|
|
return fetchJson<AgentLoginSession>(`/api/v1/agent-login/agents/${agentName}/start`, {
|
|
...options,
|
|
init: {
|
|
method: "POST",
|
|
body: JSON.stringify(size),
|
|
...(options?.init ?? {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function stopAgentLogin(sessionID: string): Promise<void> {
|
|
await fetchJson<{ ok: boolean }>(`/api/v1/agent-login/sessions/${sessionID}/stop`, {
|
|
init: { method: "POST" },
|
|
});
|
|
}
|
|
|
|
export async function resizeAgentLogin(
|
|
sessionID: string,
|
|
size: { cols: number; rows: number },
|
|
): Promise<void> {
|
|
await fetchJson<{ ok: boolean }>(`/api/v1/agent-login/sessions/${sessionID}/resize`, {
|
|
init: { method: "POST", body: JSON.stringify(size) },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Build the bi-directional WS URL for streaming a login session.
|
|
* Derives the host from the backend config (NOT window.location) so dev mode
|
|
* — where the browser is on :37429 and the API is on :38429 — routes to the
|
|
* Go backend, not the web dev server.
|
|
*/
|
|
export function agentLoginStreamUrl(sessionID: string): string {
|
|
const { apiBaseUrl } = getBackendConfig();
|
|
const url = new URL(apiBaseUrl);
|
|
const proto = url.protocol === "https:" ? "wss:" : "ws:";
|
|
return `${proto}//${url.host}/api/v1/agent-login/sessions/${sessionID}/stream`;
|
|
}
|
|
|
|
/**
|
|
* Start a plain host shell PTY (spawns $SHELL, or bash/sh fallback). Reuses
|
|
* the same session manager as agent-login, so stop/resize/stream all use the
|
|
* same session-ID-based endpoints.
|
|
*/
|
|
export async function startHostShell(
|
|
size: { cols: number; rows: number },
|
|
options?: ApiRequestOptions,
|
|
): Promise<AgentLoginSession> {
|
|
return fetchJson<AgentLoginSession>("/api/v1/host-shell/start", {
|
|
...options,
|
|
init: {
|
|
method: "POST",
|
|
body: JSON.stringify(size),
|
|
...(options?.init ?? {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function createCustomTUIAgent(
|
|
payload: { display_name: string; model?: string; command: string; description?: string },
|
|
options?: ApiRequestOptions,
|
|
): Promise<Agent> {
|
|
const res = await fetchJson<Agent>("/api/v1/agents/tui", {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
return normalizeAgentResponse(res);
|
|
}
|
|
|
|
export async function fetchDynamicModels(
|
|
agentName: string,
|
|
options?: ApiRequestOptions & { refresh?: boolean },
|
|
): Promise<DynamicModelsResponse> {
|
|
const refresh = options?.refresh ?? false;
|
|
const url = `/api/v1/agent-models/${agentName}${refresh ? "?refresh=true" : ""}`;
|
|
return fetchJson<DynamicModelsResponse>(url, options);
|
|
}
|
|
|
|
// Editors
|
|
export async function listEditors(options?: ApiRequestOptions) {
|
|
return fetchJson<EditorsResponse>("/api/v1/editors", options);
|
|
}
|
|
|
|
export async function createEditor(
|
|
payload: {
|
|
name: string;
|
|
kind: string;
|
|
config?: Record<string, unknown>;
|
|
enabled?: boolean;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<EditorOption>("/api/v1/editors", {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function updateEditor(
|
|
editorId: string,
|
|
payload: {
|
|
name?: string;
|
|
kind?: string;
|
|
config?: Record<string, unknown>;
|
|
enabled?: boolean;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<EditorOption>(`/api/v1/editors/${editorId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function deleteEditor(editorId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ success: boolean }>(`/api/v1/editors/${editorId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Prompts
|
|
export async function listPrompts(options?: ApiRequestOptions) {
|
|
return fetchJson<PromptsResponse>("/api/v1/prompts", options);
|
|
}
|
|
|
|
export async function createPrompt(
|
|
payload: { name: string; content: string },
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<CustomPrompt>("/api/v1/prompts", {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function updatePrompt(
|
|
promptId: string,
|
|
payload: { name?: string; content?: string },
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<CustomPrompt>(`/api/v1/prompts/${promptId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function deletePrompt(promptId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ success: boolean }>(`/api/v1/prompts/${promptId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Notification providers
|
|
export async function listNotificationProviders(options?: ApiRequestOptions) {
|
|
return fetchJson<NotificationProvidersResponse>("/api/v1/notification-providers", options);
|
|
}
|
|
|
|
export async function createNotificationProvider(
|
|
payload: {
|
|
name: string;
|
|
type: NotificationProvider["type"];
|
|
config?: NotificationProvider["config"];
|
|
enabled?: boolean;
|
|
events?: string[];
|
|
},
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<NotificationProvider>("/api/v1/notification-providers", {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function updateNotificationProvider(
|
|
providerId: string,
|
|
payload: Partial<{
|
|
name: string;
|
|
type: NotificationProvider["type"];
|
|
config: NotificationProvider["config"];
|
|
enabled: boolean;
|
|
events: string[];
|
|
}>,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<NotificationProvider>(`/api/v1/notification-providers/${providerId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function testNotificationProvider(providerId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<void>(`/api/v1/notification-providers/${providerId}/test`, {
|
|
...options,
|
|
init: { method: "POST", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function deleteNotificationProvider(providerId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<void>(`/api/v1/notification-providers/${providerId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Docker management
|
|
|
|
export type DockerContainer = {
|
|
id: string;
|
|
name: string;
|
|
image: string;
|
|
state: string;
|
|
status: string;
|
|
started_at?: string;
|
|
labels?: Record<string, string>;
|
|
};
|
|
|
|
export function buildDockerImageUrl(payload: {
|
|
dockerfile: string;
|
|
tag: string;
|
|
build_args?: Record<string, string>;
|
|
}): string {
|
|
// Returns the URL for SSE streaming. Caller should use EventSource or fetch with streaming.
|
|
const params = new URLSearchParams();
|
|
params.set("dockerfile", payload.dockerfile);
|
|
params.set("tag", payload.tag);
|
|
if (payload.build_args) {
|
|
params.set("build_args", JSON.stringify(payload.build_args));
|
|
}
|
|
return `/api/v1/docker/build?${params.toString()}`;
|
|
}
|
|
|
|
export async function buildDockerImage(
|
|
payload: {
|
|
dockerfile: string;
|
|
tag: string;
|
|
build_args?: Record<string, string>;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
): Promise<Response> {
|
|
const baseUrl = options?.baseUrl ?? getBackendConfig().apiBaseUrl;
|
|
return fetch(`${baseUrl}/api/v1/docker/build`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
...(options?.init ?? {}),
|
|
});
|
|
}
|
|
|
|
export async function listDockerContainers(
|
|
filter?: { image?: string; labels?: Record<string, string> },
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ containers: DockerContainer[] }> {
|
|
const params = new URLSearchParams();
|
|
if (filter?.image) params.set("image", filter.image);
|
|
if (filter?.labels) {
|
|
const labelPairs = Object.entries(filter.labels).map(([k, v]) => `${k}=${v}`);
|
|
params.set("labels", labelPairs.join(","));
|
|
}
|
|
const qs = params.toString();
|
|
return fetchJson<{ containers: DockerContainer[] }>(
|
|
`/api/v1/docker/containers${qs ? `?${qs}` : ""}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function stopDockerContainer(id: string, options?: ApiRequestOptions): Promise<void> {
|
|
await fetchJson<{ success: boolean }>(`/api/v1/docker/containers/${id}/stop`, {
|
|
...options,
|
|
init: { method: "POST", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function removeDockerContainer(
|
|
id: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<void> {
|
|
await fetchJson<{ success: boolean }>(`/api/v1/docker/containers/${id}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Remote auth methods (for remote executors like Sprites)
|
|
|
|
export type RemoteAuthMethod = {
|
|
method_id: string;
|
|
type: "env" | "files" | "gh_cli_token";
|
|
env_var?: string;
|
|
setup_hint?: string;
|
|
source_files?: string[];
|
|
target_rel_dir?: string;
|
|
label?: string;
|
|
has_local_files?: boolean;
|
|
};
|
|
|
|
export type RemoteAuthSpec = {
|
|
id: string;
|
|
display_name: string;
|
|
methods: RemoteAuthMethod[];
|
|
};
|
|
|
|
export type ListRemoteCredentialsResponse = {
|
|
auth_specs: RemoteAuthSpec[];
|
|
};
|
|
|
|
export async function listRemoteCredentials(
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListRemoteCredentialsResponse> {
|
|
return fetchJson<ListRemoteCredentialsResponse>("/api/v1/remote-credentials", options);
|
|
}
|
|
|
|
export type LocalGitIdentityResponse = {
|
|
user_name: string;
|
|
user_email: string;
|
|
detected: boolean;
|
|
};
|
|
|
|
export async function fetchLocalGitIdentity(
|
|
options?: ApiRequestOptions,
|
|
): Promise<LocalGitIdentityResponse> {
|
|
return fetchJson<LocalGitIdentityResponse>("/api/v1/git/identity", options);
|
|
}
|
|
|
|
// Sprites network policies
|
|
|
|
export type NetworkPolicyRule = {
|
|
domain: string;
|
|
action: "allow" | "deny";
|
|
include?: string;
|
|
};
|
|
|
|
export type NetworkPolicy = {
|
|
rules: NetworkPolicyRule[];
|
|
};
|
|
|
|
export async function getSpritesNetworkPolicies(
|
|
secretId: string,
|
|
spriteName: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<NetworkPolicy> {
|
|
const params = new URLSearchParams({ secret_id: secretId, sprite_name: spriteName });
|
|
return fetchJson<NetworkPolicy>(`/api/v1/sprites/network-policies?${params.toString()}`, options);
|
|
}
|
|
|
|
export async function updateSpritesNetworkPolicies(
|
|
secretId: string,
|
|
spriteName: string,
|
|
policy: NetworkPolicy,
|
|
options?: ApiRequestOptions,
|
|
): Promise<void> {
|
|
const params = new URLSearchParams({ secret_id: secretId, sprite_name: spriteName });
|
|
await fetchJson<void>(`/api/v1/sprites/network-policies?${params.toString()}`, {
|
|
...options,
|
|
init: { method: "PUT", body: JSON.stringify(policy), ...(options?.init ?? {}) },
|
|
});
|
|
}
|