The platform no longer exposes a public /api/* REST surface. The original HTTP route handlers are preserved verbatim as the single source of truth inside an internal, never-socket-bound FastAPI app (rpc_app), and are now reached only via RPC-over-WebSocket dispatch. - realtime router: anonymous connections allowed only to /api/auth/login and /api/auth/signup; all other RPCs/subscribes refused until token auth. - RPC dispatch re-injects the bearer token and X-Request-ID, then forwards through httpx.ASGITransport against the internal app (no network hop). - Public app now serves only /, /health, /health/ready and /api/ws; docs/openapi disabled. - Frontend: apiRequest routes /api/* to ws.rpc(); /health probes keep fetch. RealtimeClient gains rpc() with id correlation, timeout and AbortSignal. - Tests repointed to get_rpc_app(); anonymous-auth behaviour covered. Result: zero REST on the public surface; 264 backend tests + frontend typecheck pass.
765 lines
17 KiB
TypeScript
765 lines
17 KiB
TypeScript
/**
|
|
* Shared API contracts.
|
|
*
|
|
* These types mirror the FastAPI response schemas. They are intentionally
|
|
* hand-written and small: the backend is the source of truth, and a drift here
|
|
* surfaces immediately as a TypeScript error at the call site.
|
|
*/
|
|
|
|
export type Role = "owner" | "admin" | "manager" | "member" | "viewer";
|
|
|
|
export type MembershipStatus = "active" | "invited" | "suspended";
|
|
|
|
/**
|
|
* Provider kinds the AI Control Plane can talk to. agy (Google Antigravity)
|
|
* is the only one — the platform no longer has a provider picker.
|
|
*/
|
|
export type ProviderType = "agy";
|
|
|
|
/** agy reasoning efforts, cheapest first. Mirrors the backend's list. */
|
|
export type ReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
|
|
export const REASONING_EFFORTS: ReasoningEffort[] = [
|
|
"minimal",
|
|
"low",
|
|
"medium",
|
|
"high",
|
|
"xhigh",
|
|
];
|
|
|
|
const EFFORT_LABELS: Record<ReasoningEffort, string> = {
|
|
minimal: "حداقل",
|
|
low: "کم",
|
|
medium: "متوسط",
|
|
high: "زیاد",
|
|
xhigh: "بسیار زیاد",
|
|
};
|
|
|
|
/**
|
|
* Reasoning efforts a model may be configured with, or an empty list when the
|
|
* model cannot think — the backend rejects a thinking config everywhere else.
|
|
* Same marker rule as the agy adapter: thinking-capable ids contain one of
|
|
* `thinking` / `gemini-3` / `gemini-2.5-pro` / `gemini-2.5-flash`, image
|
|
* and claude models are excluded from explicit effort selection.
|
|
*/
|
|
export function effortsForModel(modelId: string): ReasoningEffort[] {
|
|
const lower = modelId.toLowerCase();
|
|
if (lower.includes("image")) return [];
|
|
if (lower.includes("claude")) return [];
|
|
if (lower.includes("gpt")) return ["medium"];
|
|
if (lower.includes("gemini-3.1-pro")) return ["low", "high"];
|
|
const canThink =
|
|
lower.includes("thinking") ||
|
|
lower.includes("gemini-3") ||
|
|
lower.includes("gemini-2.5-pro") ||
|
|
lower.includes("gemini-2.5-flash");
|
|
return canThink ? [...REASONING_EFFORTS] : [];
|
|
}
|
|
|
|
export function effortLabel(effort: string): string {
|
|
return EFFORT_LABELS[effort as ReasoningEffort] ?? effort;
|
|
}
|
|
|
|
/** A live model reported by an agy account's catalogue. */
|
|
export interface AgyAccountModel {
|
|
id: string;
|
|
display_name: string;
|
|
supports_thinking: boolean;
|
|
supported_efforts: ReasoningEffort[];
|
|
default_effort?: ReasoningEffort | null;
|
|
supports_images?: boolean;
|
|
}
|
|
|
|
/** Rotation-pool lifecycle of one agy account. */
|
|
export type AgyAccountStatus = "active" | "cooldown" | "error";
|
|
|
|
/**
|
|
* One agy account (Google identity). Deliberately credential-free: the console
|
|
* manages accounts, never tokens.
|
|
*/
|
|
export interface AgyAccount {
|
|
id: string;
|
|
provider_id: string;
|
|
name: string;
|
|
email: string;
|
|
project_id: string;
|
|
status: AgyAccountStatus;
|
|
cooldown_until: string | null;
|
|
is_active: boolean;
|
|
last_error: string;
|
|
last_used_at: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
/** What a model can do. Routing picks models by required capability. */
|
|
export type Capability = "text" | "vision" | "tools" | "reasoning" | "long_context" | "code";
|
|
|
|
/** Routing intents. Customers never see these names — only the platform owner does. */
|
|
export type RoutingMode = "fast" | "cheap" | "balanced" | "reasoning" | "vision" | "coding";
|
|
|
|
export type TaskStatus =
|
|
| "new"
|
|
| "triaged"
|
|
| "waiting_approval"
|
|
| "running"
|
|
| "done"
|
|
| "failed"
|
|
| "cancelled";
|
|
|
|
export type RunStatus =
|
|
| "queued"
|
|
| "planning"
|
|
| "waiting_approval"
|
|
| "running"
|
|
| "tool_calling"
|
|
| "sandbox_running"
|
|
| "completed"
|
|
| "failed"
|
|
| "cancelled"
|
|
| "timed_out";
|
|
|
|
export type RoleKey =
|
|
| "manager"
|
|
| "planner"
|
|
| "developer"
|
|
| "reviewer"
|
|
| "tester"
|
|
| "researcher"
|
|
| "content_creator"
|
|
| "support";
|
|
|
|
export type EmployeeStatus = "active" | "paused" | "archived";
|
|
|
|
export type PreviewDomainStatus = "active" | "disabled" | "failed";
|
|
|
|
export type RateUnit =
|
|
| "model_input_tokens"
|
|
| "model_output_tokens"
|
|
| "sandbox_cpu_second"
|
|
| "sandbox_memory_gb_second"
|
|
| "storage_gb_day"
|
|
| "tool_call"
|
|
| "premium_employee_run";
|
|
|
|
export interface ApiErrorBody {
|
|
error: {
|
|
code: string;
|
|
message: string;
|
|
details?: unknown;
|
|
/**
|
|
* Correlates this response with the row the API wrote to its error console
|
|
* (and with the `X-Request-ID` response header). Surfacing it lets a user
|
|
* quote one string that points at exactly one server-side record.
|
|
*/
|
|
request_id?: string;
|
|
};
|
|
}
|
|
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
avatar_url?: string | null;
|
|
is_platform_owner: boolean;
|
|
}
|
|
|
|
export interface Organization {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
owner_user_id: string;
|
|
}
|
|
|
|
export interface OrganizationWithRole extends Organization {
|
|
role: Role;
|
|
}
|
|
|
|
export interface Member {
|
|
id: string;
|
|
user_id: string;
|
|
email: string;
|
|
name: string;
|
|
role: Role;
|
|
status: MembershipStatus;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Invite {
|
|
id: string;
|
|
email: string;
|
|
role: Role;
|
|
expires_at: string;
|
|
invite_token: string;
|
|
invite_url: string;
|
|
}
|
|
|
|
export interface TokenResponse {
|
|
access_token: string;
|
|
token_type: string;
|
|
user: User;
|
|
}
|
|
|
|
export interface MeResponse {
|
|
user: User;
|
|
organizations: OrganizationWithRole[];
|
|
}
|
|
|
|
export interface Project {
|
|
id: string;
|
|
organization_id: string;
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
default_branch: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface RepoStatus {
|
|
exists: boolean;
|
|
private: boolean;
|
|
default_branch: string;
|
|
branches: string[];
|
|
open_pull_requests: number;
|
|
}
|
|
|
|
export interface Repository {
|
|
id: string;
|
|
provider: string;
|
|
owner: string;
|
|
repo_name: string;
|
|
clone_url: string;
|
|
web_url: string;
|
|
default_branch: string;
|
|
private: boolean;
|
|
}
|
|
|
|
export interface AdminModel {
|
|
id: string;
|
|
provider_id: string;
|
|
internal_name: string;
|
|
display_name_for_admin: string;
|
|
capabilities: string[];
|
|
input_price_per_1m: number;
|
|
output_price_per_1m: number;
|
|
/** agy reasoning effort ("" = provider default). Owner-only metadata. */
|
|
reasoning_effort: string;
|
|
is_active: boolean;
|
|
}
|
|
|
|
export interface AdminRoutingPolicy {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
mode: string;
|
|
is_default: boolean;
|
|
is_active: boolean;
|
|
}
|
|
|
|
export interface AdminRateCard {
|
|
id: string;
|
|
name: string;
|
|
unit: RateUnit;
|
|
customer_price: number;
|
|
internal_cost_estimate: number;
|
|
is_active: boolean;
|
|
}
|
|
|
|
export interface AdminOrganization {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
member_count: number;
|
|
created_at: string;
|
|
}
|
|
|
|
/** Result of a routing dry-run: which model a mode would pick, and why. */
|
|
export interface ModelChoice {
|
|
mode: string;
|
|
model_id: string;
|
|
internal_name: string;
|
|
capabilities: string[];
|
|
estimated_input_price_per_1m: number;
|
|
estimated_output_price_per_1m: number;
|
|
}
|
|
|
|
export interface AdminUsageRow {
|
|
organization_id: string;
|
|
quantity: number;
|
|
total_price: number;
|
|
}
|
|
|
|
export interface PreviewDomain {
|
|
id: string;
|
|
host: string;
|
|
port: number;
|
|
status: PreviewDomainStatus;
|
|
protected: boolean;
|
|
url: string;
|
|
}
|
|
|
|
export interface Employee {
|
|
id: string;
|
|
organization_id: string;
|
|
project_id?: string | null;
|
|
template_id?: string | null;
|
|
name: string;
|
|
role_key: RoleKey;
|
|
status: EmployeeStatus;
|
|
routing_policy_id?: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Task {
|
|
id: string;
|
|
organization_id: string;
|
|
project_id?: string | null;
|
|
thread_id?: string | null;
|
|
title: string;
|
|
description: string;
|
|
status: TaskStatus;
|
|
assigned_employee_id?: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface Run {
|
|
id: string;
|
|
task_id: string;
|
|
employee_id?: string | null;
|
|
status: RunStatus;
|
|
error?: string | null;
|
|
started_at?: string | null;
|
|
finished_at?: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Thread {
|
|
id: string;
|
|
organization_id: string;
|
|
project_id?: string | null;
|
|
title: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Message {
|
|
id: string;
|
|
thread_id: string;
|
|
role: "user" | "assistant" | "system";
|
|
content: string;
|
|
employee_id?: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ThreadDetail extends Thread {
|
|
messages: Message[];
|
|
}
|
|
|
|
export interface RunStep {
|
|
id: string;
|
|
index: number;
|
|
kind: string;
|
|
title: string;
|
|
payload_json: Record<string, unknown>;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ToolCall {
|
|
id: string;
|
|
tool_key: string;
|
|
status: string;
|
|
requires_approval: boolean;
|
|
arguments_json: Record<string, unknown>;
|
|
result_json: Record<string, unknown>;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ArtifactDetail extends Artifact {
|
|
content_ref: string;
|
|
run_id?: string | null;
|
|
}
|
|
|
|
export interface RunDetail extends Run {
|
|
steps: RunStep[];
|
|
tool_calls: ToolCall[];
|
|
artifacts: ArtifactDetail[];
|
|
}
|
|
|
|
export interface TaskDetail extends Task {
|
|
runs: Run[];
|
|
}
|
|
|
|
export interface AgentTemplate {
|
|
id: string;
|
|
name: string;
|
|
role_key: RoleKey;
|
|
description: string;
|
|
default_tools_json: string[];
|
|
}
|
|
|
|
/** A routing policy as a customer may see it — no mode, no model hints. */
|
|
export interface RoutingChoice {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}
|
|
|
|
export interface PromptVersion {
|
|
id: string;
|
|
version: number;
|
|
content: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ToolPermission {
|
|
tool_key: string;
|
|
allowed: boolean;
|
|
}
|
|
|
|
/** One entry in the tool catalog an employee can be granted. */
|
|
export interface ToolSpec {
|
|
key: string;
|
|
label: string;
|
|
description: string;
|
|
requires_approval: boolean;
|
|
destructive: boolean;
|
|
category: string;
|
|
}
|
|
|
|
export interface EmployeeDetail extends Employee {
|
|
prompt_versions: PromptVersion[];
|
|
tool_permissions: ToolPermission[];
|
|
open_task_count: number;
|
|
}
|
|
|
|
export interface PostMessageResponse {
|
|
user_message: Message;
|
|
assistant_message: Message;
|
|
}
|
|
|
|
export interface Artifact {
|
|
id: string;
|
|
name: string;
|
|
kind: string;
|
|
size_bytes: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface BillingSummary {
|
|
balance: number;
|
|
currency: string;
|
|
low_balance: boolean;
|
|
low_balance_threshold: number;
|
|
total_spend: number;
|
|
by_unit: { unit: RateUnit; quantity: number; total_price: number }[];
|
|
by_employee: Record<string, number>;
|
|
by_project: Record<string, number>;
|
|
}
|
|
|
|
export interface UsageEvent {
|
|
id: string;
|
|
unit: RateUnit;
|
|
quantity: number;
|
|
unit_price: number;
|
|
total_price: number;
|
|
project_id?: string | null;
|
|
employee_id?: string | null;
|
|
run_id?: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Invoice {
|
|
id: string;
|
|
amount: number;
|
|
currency: string;
|
|
status: string;
|
|
period_start?: string | null;
|
|
period_end?: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Payment {
|
|
id: string;
|
|
amount: number;
|
|
currency: string;
|
|
status: string;
|
|
provider: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Wallet {
|
|
balance: number;
|
|
currency: string;
|
|
low_balance: boolean;
|
|
}
|
|
|
|
export interface AuditEvent {
|
|
id: string;
|
|
action: string;
|
|
actor_user_id?: string | null;
|
|
target_type: string;
|
|
target_id: string;
|
|
created_at: string;
|
|
}
|
|
|
|
// --- sandbox ----------------------------------------------------------------
|
|
|
|
export type NetworkPolicy = "deny_all" | "allowlist" | "open";
|
|
|
|
export interface SandboxTemplate {
|
|
id: string;
|
|
name: string;
|
|
provider: string;
|
|
image: string;
|
|
description: string;
|
|
default_cpu: number;
|
|
default_memory_mb: number;
|
|
default_disk_mb: number;
|
|
}
|
|
|
|
export interface SandboxWorkspace {
|
|
id: string;
|
|
project_id: string;
|
|
template_id: string;
|
|
provider: string;
|
|
status: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface SandboxSession {
|
|
id: string;
|
|
workspace_id: string;
|
|
status: string;
|
|
started_at: string;
|
|
last_active_at: string;
|
|
}
|
|
|
|
export interface SandboxCommandRun {
|
|
id: string;
|
|
command: string;
|
|
exit_code: number | null;
|
|
stdout: string;
|
|
stderr: string;
|
|
duration_ms: number;
|
|
truncated: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface SandboxFileRead {
|
|
path: string;
|
|
content: string;
|
|
}
|
|
|
|
export interface SandboxSnapshot {
|
|
id: string;
|
|
name: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface SandboxPreviewUrl {
|
|
port: number;
|
|
url: string;
|
|
}
|
|
|
|
/**
|
|
* Effective limits are resolved server-side (project -> organization ->
|
|
* platform) and clamped. The UI only ever displays them.
|
|
*/
|
|
export interface SandboxResourceLimit {
|
|
cpu: number;
|
|
memory_mb: number;
|
|
disk_mb: number;
|
|
timeout_seconds: number;
|
|
max_output_bytes: number;
|
|
network_policy: NetworkPolicy;
|
|
}
|
|
|
|
export interface HealthResponse {
|
|
status: string;
|
|
app: string;
|
|
env: string;
|
|
}
|
|
|
|
export interface ReadinessResponse {
|
|
status: string;
|
|
checks: {
|
|
database: string;
|
|
model_provider: string;
|
|
adapters: Record<string, string>;
|
|
};
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Error console (platform owner) */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
export type ErrorSeverity = "warning" | "error" | "critical";
|
|
export type ErrorSource = "http" | "worker" | "startup";
|
|
export type ErrorStatusFilter = "open" | "resolved";
|
|
|
|
/**
|
|
* One persisted failure. Deliberately wide: a record that lacks the request
|
|
* path or the exception class forces whoever is debugging to reproduce the bug
|
|
* just to learn what it was.
|
|
*/
|
|
export interface ErrorEvent {
|
|
id: string;
|
|
kind: string;
|
|
message: string;
|
|
stack: string;
|
|
severity: ErrorSeverity;
|
|
source: ErrorSource;
|
|
error_type: string;
|
|
fingerprint: string;
|
|
status_code: number | null;
|
|
method: string;
|
|
path: string;
|
|
organization_id: string | null;
|
|
user_id: string | null;
|
|
run_id: string | null;
|
|
request_id: string;
|
|
ip: string;
|
|
user_agent: string;
|
|
metadata_json: Record<string, unknown>;
|
|
resolved_at: string | null;
|
|
resolved_note: string;
|
|
created_at: string;
|
|
}
|
|
|
|
/** Every occurrence of one distinct bug, collapsed into a single row. */
|
|
export interface ErrorGroup {
|
|
fingerprint: string;
|
|
occurrences: number;
|
|
open_count: number;
|
|
first_seen: string;
|
|
last_seen: string;
|
|
kind: string;
|
|
error_type: string;
|
|
severity: ErrorSeverity;
|
|
source: ErrorSource;
|
|
path: string;
|
|
sample_message: string;
|
|
}
|
|
|
|
export interface ErrorSummary {
|
|
open_by_severity: Record<string, number>;
|
|
total_open: number;
|
|
}
|
|
|
|
export interface ErrorListResponse {
|
|
items: ErrorEvent[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
}
|
|
|
|
export interface ResolveErrorResponse {
|
|
fingerprint: string;
|
|
resolved: number;
|
|
}
|
|
|
|
export interface ErrorQuery {
|
|
status?: ErrorStatusFilter;
|
|
severity?: ErrorSeverity;
|
|
source?: ErrorSource;
|
|
organization_id?: string;
|
|
search?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Realtime (WebSocket)
|
|
//
|
|
// The live feed is the same product surface as the REST API: one envelope
|
|
// shape, mirrored key-for-key from ``app/application/ports/realtime.py``.
|
|
// A page that switches on ``WsEventType`` never needs to know which part of
|
|
// the backend produced the event.
|
|
//
|
|
// Channel naming is what a page subscribes to — ``org:{id}`` for the tenant
|
|
// feed, ``task:{id}`` for one task, and so on. The server authorizes every
|
|
// subscribe against the caller's memberships, so a channel the user may not
|
|
// see simply comes back rejected.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type WsEventType =
|
|
| "run.step"
|
|
| "run.status"
|
|
| "task.status"
|
|
| "task.created"
|
|
| "chat.pending"
|
|
| "message.created"
|
|
| "approval.pending"
|
|
| "artifact.created"
|
|
| "sandbox.command"
|
|
| "sandbox.session"
|
|
| "sandbox.reaped"
|
|
| "usage.recorded"
|
|
| "billing.wallet"
|
|
| "error.recorded"
|
|
| "audit.recorded"
|
|
| "worker.status";
|
|
|
|
/** One pushed event, as the browser receives it. */
|
|
export interface WsEvent<T = Record<string, unknown>> {
|
|
id: string;
|
|
type: WsEventType;
|
|
data: T;
|
|
organization_id: string | null;
|
|
/** ISO-8601 UTC; render with ``toLocaleString`` like every other timestamp. */
|
|
at: string;
|
|
}
|
|
|
|
/** Every frame the server sends. Control frames and events share ``type``. */
|
|
export type WsServerFrame =
|
|
| { type: "hello"; user_id: string | null; server_time: string }
|
|
| { type: "subscribed"; accepted: string[]; rejected: string[] }
|
|
| { type: "unsubscribed"; channels: string[] }
|
|
| { type: "pong" }
|
|
| { type: "error"; message: string }
|
|
| { type: "event"; event: WsEvent }
|
|
/** Reply to a client ``rpc`` frame: the HTTP status and parsed JSON body of the
|
|
* internal dispatch, correlating on ``id``. */
|
|
| { type: "rpc_result"; id: string; status: number; body: unknown };
|
|
|
|
/** Every frame the client may send. */
|
|
export type WsClientFrame =
|
|
| { action: "subscribe"; channels: string[] }
|
|
| { action: "unsubscribe"; channels: string[] }
|
|
| { action: "ping" }
|
|
/** A request/response call into the server. ``id`` correlates the
|
|
* ``rpc_result``; ``path`` is the full mounted path including any query
|
|
* string. Every application call travels over this frame. */
|
|
| {
|
|
action: "rpc";
|
|
id: string;
|
|
method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
path: string;
|
|
body: unknown;
|
|
};
|
|
|
|
/** Live run-step payload — what the task timeline renders as it arrives. */
|
|
export interface WsRunStepData {
|
|
run_id: string;
|
|
task_id: string;
|
|
step: {
|
|
id: string;
|
|
index: number;
|
|
kind: string;
|
|
title: string;
|
|
payload: Record<string, unknown>;
|
|
};
|
|
}
|
|
|
|
/** Live sandbox command payload — the terminal panel's stream. */
|
|
export interface WsSandboxCommandData {
|
|
session_id: string;
|
|
run_id: string;
|
|
command: string;
|
|
status: string;
|
|
exit_code: number | null;
|
|
stdout: string;
|
|
stderr: string;
|
|
duration_ms: number;
|
|
truncated: boolean;
|
|
}
|