forked from wrenn/wrenn
Rename API routes /v1/sandboxes → /v1/capsules
This commit is contained in:
@ -68,7 +68,7 @@ envd is a **completely independent Go module**. It is never imported by the main
|
||||
|
||||
Startup (`cmd/control-plane/main.go`) wires: config (env vars) → pgxpool → `db.Queries` (sqlc-generated) → Connect RPC client to host agent → `api.Server`. Everything flows through constructor injection.
|
||||
|
||||
- **API Server** (`internal/api/server.go`): chi router with middleware. Creates handler structs (`sandboxHandler`, `execHandler`, `filesHandler`, etc.) injected with `db.Queries` and the host agent Connect RPC client. Routes under `/v1/sandboxes/*`.
|
||||
- **API Server** (`internal/api/server.go`): chi router with middleware. Creates handler structs (`sandboxHandler`, `execHandler`, `filesHandler`, etc.) injected with `db.Queries` and the host agent Connect RPC client. Routes under `/v1/capsules/*`.
|
||||
- **Reconciler** (`internal/api/reconciler.go`): background goroutine (every 30s) that compares DB records against `agent.ListSandboxes()` RPC. Marks orphaned DB entries as "stopped".
|
||||
- **Dashboard** (SvelteKit + Tailwind + Bits UI, statically built and embedded via `go:embed`, served as catch-all at root)
|
||||
- **Database**: PostgreSQL via pgx/v5. Queries generated by sqlc from `db/queries/sandboxes.sql`. Migrations in `db/migrations/` (goose, plain SQL).
|
||||
@ -147,19 +147,19 @@ HIBERNATED → RUNNING (cold snapshot resume, slower)
|
||||
|
||||
### Key Request Flows
|
||||
|
||||
**Sandbox creation** (`POST /v1/sandboxes`):
|
||||
**Sandbox creation** (`POST /v1/capsules`):
|
||||
1. API handler generates sandbox ID, inserts into DB as "pending"
|
||||
2. RPC `CreateSandbox` → host agent → `sandbox.Manager.Create()`
|
||||
3. Manager: resolve base rootfs → acquire shared loop device → create dm-snapshot (sparse CoW file) → allocate network slot → `CreateNetwork()` (netns + veth + tap + NAT) → `vm.Create()` (start Firecracker with `/dev/mapper/wrenn-{id}`, configure via HTTP API, boot) → `envdclient.WaitUntilReady()` (poll /health) → store in-memory state
|
||||
4. API handler updates DB to "running" with host_ip
|
||||
|
||||
**Command execution** (`POST /v1/sandboxes/{id}/exec`):
|
||||
**Command execution** (`POST /v1/capsules/{id}/exec`):
|
||||
1. API handler verifies sandbox is "running" in DB
|
||||
2. RPC `Exec` → host agent → `sandbox.Manager.Exec()` → `envdclient.Exec()`
|
||||
3. envd client opens bidirectional Connect RPC stream (`process.Start`), collects stdout/stderr/exit_code
|
||||
4. API handler checks UTF-8 validity (base64-encodes if binary), updates last_active_at, returns result
|
||||
|
||||
**Streaming exec** (`WS /v1/sandboxes/{id}/exec/stream`):
|
||||
**Streaming exec** (`WS /v1/capsules/{id}/exec/stream`):
|
||||
1. WebSocket upgrade, read first message for cmd/args
|
||||
2. RPC `ExecStream` → host agent → `sandbox.Manager.ExecStream()` → `envdclient.ExecStream()`
|
||||
3. envd client returns a channel of events; host agent forwards events through the RPC stream
|
||||
|
||||
@ -17,11 +17,11 @@ export type Capsule = {
|
||||
|
||||
|
||||
export async function listCapsules(): Promise<ApiResult<Capsule[]>> {
|
||||
return apiFetch('GET', '/api/v1/sandboxes');
|
||||
return apiFetch('GET', '/api/v1/capsules');
|
||||
}
|
||||
|
||||
export async function getCapsule(id: string): Promise<ApiResult<Capsule>> {
|
||||
return apiFetch('GET', `/api/v1/sandboxes/${id}`);
|
||||
return apiFetch('GET', `/api/v1/capsules/${id}`);
|
||||
}
|
||||
|
||||
export type CreateCapsuleParams = {
|
||||
@ -32,19 +32,19 @@ export type CreateCapsuleParams = {
|
||||
};
|
||||
|
||||
export async function createCapsule(params: CreateCapsuleParams): Promise<ApiResult<Capsule>> {
|
||||
return apiFetch('POST', '/api/v1/sandboxes', params);
|
||||
return apiFetch('POST', '/api/v1/capsules', params);
|
||||
}
|
||||
|
||||
export async function pauseCapsule(id: string): Promise<ApiResult<Capsule>> {
|
||||
return apiFetch('POST', `/api/v1/sandboxes/${id}/pause`);
|
||||
return apiFetch('POST', `/api/v1/capsules/${id}/pause`);
|
||||
}
|
||||
|
||||
export async function resumeCapsule(id: string): Promise<ApiResult<Capsule>> {
|
||||
return apiFetch('POST', `/api/v1/sandboxes/${id}/resume`);
|
||||
return apiFetch('POST', `/api/v1/capsules/${id}/resume`);
|
||||
}
|
||||
|
||||
export async function destroyCapsule(id: string): Promise<ApiResult<void>> {
|
||||
return apiFetch('DELETE', `/api/v1/sandboxes/${id}`);
|
||||
return apiFetch('DELETE', `/api/v1/capsules/${id}`);
|
||||
}
|
||||
|
||||
export type Snapshot = {
|
||||
@ -57,8 +57,8 @@ export type Snapshot = {
|
||||
platform: boolean;
|
||||
};
|
||||
|
||||
export async function createSnapshot(sandboxId: string, name?: string): Promise<ApiResult<Snapshot>> {
|
||||
return apiFetch('POST', '/api/v1/snapshots', { sandbox_id: sandboxId, name });
|
||||
export async function createSnapshot(capsuleId: string, name?: string): Promise<ApiResult<Snapshot>> {
|
||||
return apiFetch('POST', '/api/v1/snapshots', { sandbox_id: capsuleId, name });
|
||||
}
|
||||
|
||||
export async function listSnapshots(typeFilter?: string): Promise<ApiResult<Snapshot[]>> {
|
||||
|
||||
@ -53,12 +53,12 @@ export function formatFileSize(bytes: number): string {
|
||||
return `${val < 10 ? val.toFixed(1) : Math.round(val)} ${units[i]}`;
|
||||
}
|
||||
|
||||
export async function listDir(sandboxId: string, path: string, depth = 1): Promise<ApiResult<ListDirResponse>> {
|
||||
export async function listDir(capsuleId: string, path: string, depth = 1): Promise<ApiResult<ListDirResponse>> {
|
||||
try {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
|
||||
|
||||
const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/list`, {
|
||||
const res = await fetch(`/api/v1/capsules/${capsuleId}/files/list`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ path, depth }),
|
||||
@ -72,12 +72,12 @@ export async function listDir(sandboxId: string, path: string, depth = 1): Promi
|
||||
}
|
||||
}
|
||||
|
||||
export async function readFile(sandboxId: string, path: string): Promise<ApiResult<string>> {
|
||||
export async function readFile(capsuleId: string, path: string): Promise<ApiResult<string>> {
|
||||
try {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
|
||||
|
||||
const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/read`, {
|
||||
const res = await fetch(`/api/v1/capsules/${capsuleId}/files/read`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ path }),
|
||||
@ -100,11 +100,11 @@ export async function readFile(sandboxId: string, path: string): Promise<ApiResu
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFile(sandboxId: string, path: string, filename: string): Promise<void> {
|
||||
export async function downloadFile(capsuleId: string, path: string, filename: string): Promise<void> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
|
||||
|
||||
const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/read`, {
|
||||
const res = await fetch(`/api/v1/capsules/${capsuleId}/files/read`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ path }),
|
||||
|
||||
@ -15,8 +15,8 @@ export type MetricsResponse = {
|
||||
points: MetricPoint[];
|
||||
};
|
||||
|
||||
export async function fetchSandboxMetrics(id: string, range: MetricRange): Promise<ApiResult<MetricsResponse>> {
|
||||
return apiFetch('GET', `/api/v1/sandboxes/${id}/metrics?range=${range}`);
|
||||
export async function fetchCapsuleMetrics(id: string, range: MetricRange): Promise<ApiResult<MetricsResponse>> {
|
||||
return apiFetch('GET', `/api/v1/capsules/${id}/metrics?range=${range}`);
|
||||
}
|
||||
|
||||
export const METRIC_RANGES: MetricRange[] = ['5m', '10m', '1h', '6h', '24h'];
|
||||
|
||||
@ -24,7 +24,7 @@ export type StatsResponse = {
|
||||
};
|
||||
|
||||
export async function fetchStats(range: TimeRange): Promise<ApiResult<StatsResponse>> {
|
||||
return apiFetch('GET', `/api/v1/sandboxes/stats?range=${range}`);
|
||||
return apiFetch('GET', `/api/v1/capsules/stats?range=${range}`);
|
||||
}
|
||||
|
||||
export const POLL_INTERVALS: Record<TimeRange, number> = {
|
||||
|
||||
@ -11,11 +11,11 @@
|
||||
import { tokenize, type ThemedToken } from '$lib/highlight';
|
||||
|
||||
type Props = {
|
||||
sandboxId: string;
|
||||
capsuleId: string;
|
||||
isRunning: boolean;
|
||||
};
|
||||
|
||||
let { sandboxId, isRunning }: Props = $props();
|
||||
let { capsuleId, isRunning }: Props = $props();
|
||||
|
||||
// Directory navigation state
|
||||
let currentPath = $state('~');
|
||||
@ -124,7 +124,7 @@
|
||||
dirLoading = true;
|
||||
dirError = null;
|
||||
const gen = ++dirGeneration;
|
||||
const result = await listDir(sandboxId, currentPath);
|
||||
const result = await listDir(capsuleId, currentPath);
|
||||
if (gen !== dirGeneration) return; // stale response
|
||||
if (result.ok) {
|
||||
entries = result.data.entries ?? [];
|
||||
@ -159,7 +159,7 @@
|
||||
|
||||
fileLoading = true;
|
||||
const gen = ++fileGeneration;
|
||||
const result = await readFile(sandboxId, entry.path);
|
||||
const result = await readFile(capsuleId, entry.path);
|
||||
if (gen !== fileGeneration) return; // stale response — user clicked another file
|
||||
if (result.ok) {
|
||||
if (looksLikeBinary(result.data)) {
|
||||
@ -197,7 +197,7 @@
|
||||
if (!selectedFile || downloading) return;
|
||||
downloading = true;
|
||||
try {
|
||||
await downloadFile(sandboxId, selectedFile.path, selectedFile.name);
|
||||
await downloadFile(capsuleId, selectedFile.path, selectedFile.name);
|
||||
} catch {
|
||||
fileError = 'Download failed';
|
||||
}
|
||||
@ -214,7 +214,7 @@
|
||||
|
||||
async function navigateOrOpenFile(path: string) {
|
||||
// First try as directory
|
||||
const dirResult = await listDir(sandboxId, path);
|
||||
const dirResult = await listDir(capsuleId, path);
|
||||
if (dirResult.ok) {
|
||||
// Resolve actual path from entries (handles ~ expansion by envd)
|
||||
const resolvedEntries = dirResult.data.entries ?? [];
|
||||
@ -245,7 +245,7 @@
|
||||
// Navigate to parent directory
|
||||
currentPath = parentPath;
|
||||
pathInput = parentPath;
|
||||
const parentResult = await listDir(sandboxId, parentPath);
|
||||
const parentResult = await listDir(capsuleId, parentPath);
|
||||
if (parentResult.ok) {
|
||||
entries = parentResult.data.entries ?? [];
|
||||
// Find the file in parent listing
|
||||
|
||||
@ -3,12 +3,12 @@
|
||||
import { auth } from '$lib/auth.svelte';
|
||||
|
||||
type Props = {
|
||||
sandboxId: string;
|
||||
capsuleId: string;
|
||||
isRunning: boolean;
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
let { sandboxId, isRunning, visible = true }: Props = $props();
|
||||
let { capsuleId, isRunning, visible = true }: Props = $props();
|
||||
|
||||
type ConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
function getWsUrl(): string {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const token = auth.token ? `?token=${encodeURIComponent(auth.token)}` : '';
|
||||
return `${proto}//${window.location.host}/api/v1/sandboxes/${sandboxId}/pty${token}`;
|
||||
return `${proto}//${window.location.host}/api/v1/capsules/${capsuleId}/pty${token}`;
|
||||
}
|
||||
|
||||
function wsSend(ws: WebSocket | null, data: string) {
|
||||
|
||||
@ -17,7 +17,7 @@ let loadingPromise: Promise<HighlighterGeneric<any, any>> | null = null;
|
||||
const THEME = 'vesper';
|
||||
|
||||
// Extensions → shiki language IDs.
|
||||
// Only map what we expect users to encounter in sandboxes.
|
||||
// Only map what we expect users to encounter in capsules.
|
||||
const EXT_TO_LANG: Record<string, string> = {
|
||||
// Go
|
||||
go: 'go', mod: 'go', sum: 'go',
|
||||
|
||||
@ -77,7 +77,7 @@
|
||||
|
||||
// Delete confirmation
|
||||
let deleteTarget = $state<Host | null>(null);
|
||||
let deletePreviewSandboxes = $state<string[]>([]);
|
||||
let deletePreviewCapsules = $state<string[]>([]);
|
||||
let deletePreviewLoading = $state(false);
|
||||
let deleting = $state(false);
|
||||
let deleteError = $state<string | null>(null);
|
||||
@ -124,12 +124,12 @@
|
||||
async function openDeleteConfirm(host: Host) {
|
||||
deleteTarget = host;
|
||||
deleteError = null;
|
||||
deletePreviewSandboxes = [];
|
||||
deletePreviewCapsules = [];
|
||||
deletePreviewLoading = true;
|
||||
const preview = await getDeletePreview(host.id);
|
||||
deletePreviewLoading = false;
|
||||
if (preview.ok) {
|
||||
deletePreviewSandboxes = preview.data.sandbox_ids;
|
||||
deletePreviewCapsules = preview.data.sandbox_ids;
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@
|
||||
if (!deleteTarget) return;
|
||||
deleting = true;
|
||||
deleteError = null;
|
||||
const result = await deleteHost(deleteTarget.id, deletePreviewSandboxes.length > 0);
|
||||
const result = await deleteHost(deleteTarget.id, deletePreviewCapsules.length > 0);
|
||||
if (result.ok) {
|
||||
allHosts = allHosts.filter((h) => h.id !== deleteTarget!.id);
|
||||
deleteTarget = null;
|
||||
@ -627,10 +627,10 @@
|
||||
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
Checking active capsules…
|
||||
</div>
|
||||
{:else if deletePreviewSandboxes.length > 0}
|
||||
{:else if deletePreviewCapsules.length > 0}
|
||||
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-amber)]/30 bg-[var(--color-amber)]/6 px-3 py-2.5">
|
||||
<p class="text-meta font-semibold text-[var(--color-amber)]">
|
||||
{deletePreviewSandboxes.length} active capsule{deletePreviewSandboxes.length === 1 ? '' : 's'} will be destroyed.
|
||||
{deletePreviewCapsules.length} active capsule{deletePreviewCapsules.length === 1 ? '' : 's'} will be destroyed.
|
||||
</p>
|
||||
<p class="mt-0.5 text-meta text-[var(--color-amber)]/70">
|
||||
All running workloads on this host will be terminated immediately.
|
||||
@ -661,7 +661,7 @@
|
||||
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
Deleting…
|
||||
{:else}
|
||||
{deletePreviewSandboxes.length > 0 ? 'Force Delete' : 'Delete'}
|
||||
{deletePreviewCapsules.length > 0 ? 'Force Delete' : 'Delete'}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -9,14 +9,14 @@
|
||||
import FilesTab from '$lib/components/FilesTab.svelte';
|
||||
import TerminalTab from '$lib/components/TerminalTab.svelte';
|
||||
import {
|
||||
fetchSandboxMetrics,
|
||||
fetchCapsuleMetrics,
|
||||
METRIC_RANGES,
|
||||
METRIC_POLL_INTERVALS,
|
||||
type MetricRange,
|
||||
type MetricPoint
|
||||
} from '$lib/api/metrics';
|
||||
|
||||
const sandboxId: string = $page.params.id ?? '';
|
||||
const capsuleId: string = $page.params.id ?? '';
|
||||
|
||||
let capsule = $state<Capsule | null>(null);
|
||||
let capsuleLoading = $state(true);
|
||||
@ -96,7 +96,7 @@
|
||||
);
|
||||
|
||||
async function loadCapsule() {
|
||||
const result = await getCapsule(sandboxId);
|
||||
const result = await getCapsule(capsuleId);
|
||||
if (result.ok) {
|
||||
capsule = result.data;
|
||||
capsuleError = null;
|
||||
@ -108,7 +108,7 @@
|
||||
|
||||
async function loadMetrics() {
|
||||
if (!metricsAvailable) return;
|
||||
const result = await fetchSandboxMetrics(sandboxId, range);
|
||||
const result = await fetchCapsuleMetrics(capsuleId, range);
|
||||
if (result.ok) {
|
||||
points = result.data.points;
|
||||
metricsError = null;
|
||||
@ -441,7 +441,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Wrenn — {sandboxId}</title>
|
||||
<title>Wrenn — {capsuleId}</title>
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
@ -575,11 +575,11 @@
|
||||
<!-- Tab content -->
|
||||
<!-- Terminal stays mounted so sessions survive tab switches -->
|
||||
<div class="flex flex-1 min-h-0" style:display={activeTab === 'terminal' ? 'flex' : 'none'}>
|
||||
<TerminalTab sandboxId={sandboxId} isRunning={capsule.status === 'running'} visible={activeTab === 'terminal'} />
|
||||
<TerminalTab capsuleId={capsuleId} isRunning={capsule.status === 'running'} visible={activeTab === 'terminal'} />
|
||||
</div>
|
||||
{#if activeTab === 'files'}
|
||||
<div class="anim-in flex flex-1 min-h-0" style="animation-delay: 0.05s">
|
||||
<FilesTab sandboxId={sandboxId} isRunning={capsule.status === 'running'} />
|
||||
<FilesTab capsuleId={capsuleId} isRunning={capsule.status === 'running'} />
|
||||
</div>
|
||||
{:else if activeTab === 'metrics'}
|
||||
<div
|
||||
@ -757,14 +757,14 @@
|
||||
|
||||
<SnapshotDialog
|
||||
open={showSnapshot}
|
||||
capsuleId={sandboxId}
|
||||
capsuleId={capsuleId}
|
||||
onclose={() => { showSnapshot = false; }}
|
||||
onsnapshot={() => { goto('/dashboard/capsules'); }}
|
||||
/>
|
||||
|
||||
<DestroyDialog
|
||||
open={showDestroy}
|
||||
capsuleId={sandboxId}
|
||||
capsuleId={capsuleId}
|
||||
onclose={() => { showDestroy = false; }}
|
||||
ondestroyed={() => { goto('/dashboard/capsules'); }}
|
||||
/>
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
|
||||
// Delete confirmation
|
||||
let deleteTarget = $state<Host | null>(null);
|
||||
let deletePreviewSandboxes = $state<string[]>([]);
|
||||
let deletePreviewCapsules = $state<string[]>([]);
|
||||
let deletePreviewLoading = $state(false);
|
||||
let deleting = $state(false);
|
||||
let deleteError = $state<string | null>(null);
|
||||
@ -97,12 +97,12 @@
|
||||
async function openDeleteConfirm(host: Host) {
|
||||
deleteTarget = host;
|
||||
deleteError = null;
|
||||
deletePreviewSandboxes = [];
|
||||
deletePreviewCapsules = [];
|
||||
deletePreviewLoading = true;
|
||||
const preview = await getDeletePreview(host.id);
|
||||
deletePreviewLoading = false;
|
||||
if (preview.ok) {
|
||||
deletePreviewSandboxes = preview.data.sandbox_ids;
|
||||
deletePreviewCapsules = preview.data.sandbox_ids;
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@
|
||||
if (!deleteTarget) return;
|
||||
deleting = true;
|
||||
deleteError = null;
|
||||
const result = await deleteHost(deleteTarget.id, deletePreviewSandboxes.length > 0);
|
||||
const result = await deleteHost(deleteTarget.id, deletePreviewCapsules.length > 0);
|
||||
if (result.ok) {
|
||||
hosts = hosts.filter((h) => h.id !== deleteTarget!.id);
|
||||
deleteTarget = null;
|
||||
@ -583,10 +583,10 @@
|
||||
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
Checking active capsules…
|
||||
</div>
|
||||
{:else if deletePreviewSandboxes.length > 0}
|
||||
{:else if deletePreviewCapsules.length > 0}
|
||||
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-amber)]/20 bg-[var(--color-amber)]/5 px-3 py-2.5">
|
||||
<p class="text-meta font-semibold text-[var(--color-amber)]">
|
||||
{deletePreviewSandboxes.length} active capsule{deletePreviewSandboxes.length === 1 ? '' : 's'} will be destroyed.
|
||||
{deletePreviewCapsules.length} active capsule{deletePreviewCapsules.length === 1 ? '' : 's'} will be destroyed.
|
||||
</p>
|
||||
<p class="mt-0.5 text-meta text-[var(--color-amber)]/70">
|
||||
All running workloads on this host will be terminated immediately.
|
||||
@ -617,7 +617,7 @@
|
||||
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
Deleting…
|
||||
{:else}
|
||||
{deletePreviewSandboxes.length > 0 ? 'Force Delete' : 'Delete Host'}
|
||||
{deletePreviewCapsules.length > 0 ? 'Force Delete' : 'Delete Host'}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -45,7 +45,7 @@ type execResponse struct {
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
// Exec handles POST /v1/sandboxes/{id}/exec.
|
||||
// Exec handles POST /v1/capsules/{id}/exec.
|
||||
func (h *execHandler) Exec(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ctx := r.Context()
|
||||
|
||||
@ -47,7 +47,7 @@ type wsOutMsg struct {
|
||||
ExitCode *int32 `json:"exit_code,omitempty"` // only for "exit"
|
||||
}
|
||||
|
||||
// ExecStream handles WS /v1/sandboxes/{id}/exec/stream.
|
||||
// ExecStream handles WS /v1/capsules/{id}/exec/stream.
|
||||
func (h *execStreamHandler) ExecStream(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ctx := r.Context()
|
||||
|
||||
@ -25,7 +25,7 @@ func newFilesHandler(db *db.Queries, pool *lifecycle.HostClientPool) *filesHandl
|
||||
return &filesHandler{db: db, pool: pool}
|
||||
}
|
||||
|
||||
// Upload handles POST /v1/sandboxes/{id}/files/write.
|
||||
// Upload handles POST /v1/capsules/{id}/files/write.
|
||||
// Expects multipart/form-data with:
|
||||
// - "path" text field: absolute destination path inside the sandbox
|
||||
// - "file" file field: binary content to write
|
||||
@ -105,7 +105,7 @@ type readFileRequest struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// Download handles POST /v1/sandboxes/{id}/files/read.
|
||||
// Download handles POST /v1/capsules/{id}/files/read.
|
||||
// Accepts JSON body with path, returns raw file content with Content-Disposition.
|
||||
func (h *filesHandler) Download(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
|
||||
@ -26,7 +26,7 @@ func newFilesStreamHandler(db *db.Queries, pool *lifecycle.HostClientPool) *file
|
||||
return &filesStreamHandler{db: db, pool: pool}
|
||||
}
|
||||
|
||||
// StreamUpload handles POST /v1/sandboxes/{id}/files/stream/write.
|
||||
// StreamUpload handles POST /v1/capsules/{id}/files/stream/write.
|
||||
// Expects multipart/form-data with "path" text field and "file" file field.
|
||||
// Streams file content directly from the request body to the host agent without buffering.
|
||||
func (h *filesStreamHandler) StreamUpload(w http.ResponseWriter, r *http.Request) {
|
||||
@ -150,7 +150,7 @@ func (h *filesStreamHandler) StreamUpload(w http.ResponseWriter, r *http.Request
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// StreamDownload handles POST /v1/sandboxes/{id}/files/stream/read.
|
||||
// StreamDownload handles POST /v1/capsules/{id}/files/stream/read.
|
||||
// Accepts JSON body with path, streams file content back without buffering.
|
||||
func (h *filesStreamHandler) StreamDownload(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
|
||||
@ -56,7 +56,7 @@ type removeRequest struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// ListDir handles POST /v1/sandboxes/{id}/files/list.
|
||||
// ListDir handles POST /v1/capsules/{id}/files/list.
|
||||
func (h *fsHandler) ListDir(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ctx := r.Context()
|
||||
@ -113,7 +113,7 @@ func (h *fsHandler) ListDir(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, listDirResponse{Entries: entries})
|
||||
}
|
||||
|
||||
// MakeDir handles POST /v1/sandboxes/{id}/files/mkdir.
|
||||
// MakeDir handles POST /v1/capsules/{id}/files/mkdir.
|
||||
func (h *fsHandler) MakeDir(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ctx := r.Context()
|
||||
@ -164,7 +164,7 @@ func (h *fsHandler) MakeDir(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, makeDirResponse{Entry: fileEntryFromPB(resp.Msg.Entry)})
|
||||
}
|
||||
|
||||
// Remove handles POST /v1/sandboxes/{id}/files/remove.
|
||||
// Remove handles POST /v1/capsules/{id}/files/remove.
|
||||
func (h *fsHandler) Remove(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ctx := r.Context()
|
||||
|
||||
@ -38,7 +38,7 @@ type metricsResponse struct {
|
||||
Points []metricPointResponse `json:"points"`
|
||||
}
|
||||
|
||||
// GetMetrics handles GET /v1/sandboxes/{id}/metrics?range=10m|2h|24h.
|
||||
// GetMetrics handles GET /v1/capsules/{id}/metrics?range=10m|2h|24h.
|
||||
func (h *sandboxMetricsHandler) GetMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ctx := r.Context()
|
||||
|
||||
@ -79,7 +79,7 @@ func (w *wsWriter) writeJSON(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
// PtySession handles WS /v1/sandboxes/{id}/pty.
|
||||
// PtySession handles WS /v1/capsules/{id}/pty.
|
||||
func (h *ptyHandler) PtySession(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ctx := r.Context()
|
||||
|
||||
@ -73,7 +73,7 @@ func sandboxToResponse(sb db.Sandbox) sandboxResponse {
|
||||
return resp
|
||||
}
|
||||
|
||||
// Create handles POST /v1/sandboxes.
|
||||
// Create handles POST /v1/capsules.
|
||||
func (h *sandboxHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
var req createSandboxRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@ -104,7 +104,7 @@ func (h *sandboxHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, sandboxToResponse(sb))
|
||||
}
|
||||
|
||||
// List handles GET /v1/sandboxes.
|
||||
// List handles GET /v1/capsules.
|
||||
func (h *sandboxHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
ac := auth.MustFromContext(r.Context())
|
||||
sandboxes, err := h.svc.List(r.Context(), ac.TeamID)
|
||||
@ -121,7 +121,7 @@ func (h *sandboxHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// Get handles GET /v1/sandboxes/{id}.
|
||||
// Get handles GET /v1/capsules/{id}.
|
||||
func (h *sandboxHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ac := auth.MustFromContext(r.Context())
|
||||
@ -141,7 +141,7 @@ func (h *sandboxHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, sandboxToResponse(sb))
|
||||
}
|
||||
|
||||
// Pause handles POST /v1/sandboxes/{id}/pause.
|
||||
// Pause handles POST /v1/capsules/{id}/pause.
|
||||
func (h *sandboxHandler) Pause(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ac := auth.MustFromContext(r.Context())
|
||||
@ -163,7 +163,7 @@ func (h *sandboxHandler) Pause(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, sandboxToResponse(sb))
|
||||
}
|
||||
|
||||
// Resume handles POST /v1/sandboxes/{id}/resume.
|
||||
// Resume handles POST /v1/capsules/{id}/resume.
|
||||
func (h *sandboxHandler) Resume(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ac := auth.MustFromContext(r.Context())
|
||||
@ -185,7 +185,7 @@ func (h *sandboxHandler) Resume(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, sandboxToResponse(sb))
|
||||
}
|
||||
|
||||
// Ping handles POST /v1/sandboxes/{id}/ping.
|
||||
// Ping handles POST /v1/capsules/{id}/ping.
|
||||
func (h *sandboxHandler) Ping(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ac := auth.MustFromContext(r.Context())
|
||||
@ -205,7 +205,7 @@ func (h *sandboxHandler) Ping(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Destroy handles DELETE /v1/sandboxes/{id}.
|
||||
// Destroy handles DELETE /v1/capsules/{id}.
|
||||
func (h *sandboxHandler) Destroy(w http.ResponseWriter, r *http.Request) {
|
||||
sandboxIDStr := chi.URLParam(r, "id")
|
||||
ac := auth.MustFromContext(r.Context())
|
||||
|
||||
@ -43,7 +43,7 @@ type statsResponse struct {
|
||||
Series statsSeriesResponse `json:"series"`
|
||||
}
|
||||
|
||||
// GetStats handles GET /v1/sandboxes/stats?range=5m|1h|6h|24h|30d
|
||||
// GetStats handles GET /v1/capsules/stats?range=5m|1h|6h|24h|30d
|
||||
func (h *statsHandler) GetStats(w http.ResponseWriter, r *http.Request) {
|
||||
ac := auth.MustFromContext(r.Context())
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
openapi: "3.1.0"
|
||||
info:
|
||||
title: Wrenn Sandbox API
|
||||
title: Wrenn API
|
||||
description: MicroVM-based code execution platform API.
|
||||
version: "0.1.0"
|
||||
|
||||
@ -393,7 +393,7 @@ paths:
|
||||
- bearerAuth: []
|
||||
description: |
|
||||
Owner only. Soft-deletes the team and destroys all running/paused/starting
|
||||
sandboxes. All DB records are preserved. The team slug is permanently reserved.
|
||||
capsulees. All DB records are preserved. The team slug is permanently reserved.
|
||||
responses:
|
||||
"204":
|
||||
description: Team deleted
|
||||
@ -570,11 +570,11 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes:
|
||||
/v1/capsules:
|
||||
post:
|
||||
summary: Create a sandbox
|
||||
operationId: createSandbox
|
||||
tags: [sandboxes]
|
||||
summary: Create a capsule
|
||||
operationId: createCapsule
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
requestBody:
|
||||
@ -582,14 +582,14 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateSandboxRequest"
|
||||
$ref: "#/components/schemas/CreateCapsuleRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Sandbox created
|
||||
description: Capsule created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Sandbox"
|
||||
$ref: "#/components/schemas/Capsule"
|
||||
"502":
|
||||
description: Host agent error
|
||||
content:
|
||||
@ -598,26 +598,26 @@ paths:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
get:
|
||||
summary: List sandboxes for your team
|
||||
operationId: listSandboxes
|
||||
tags: [sandboxes]
|
||||
summary: List capsulees for your team
|
||||
operationId: listCapsules
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: List of sandboxes
|
||||
description: List of capsulees
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Sandbox"
|
||||
$ref: "#/components/schemas/Capsule"
|
||||
|
||||
/v1/sandboxes/stats:
|
||||
/v1/capsules/stats:
|
||||
get:
|
||||
summary: Get sandbox usage stats for your team
|
||||
operationId: getSandboxStats
|
||||
tags: [sandboxes]
|
||||
summary: Get capsule usage stats for your team
|
||||
operationId: getCapsuleStats
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
parameters:
|
||||
@ -631,15 +631,15 @@ paths:
|
||||
description: Time window for the time-series data.
|
||||
responses:
|
||||
"200":
|
||||
description: Sandbox stats for the team
|
||||
description: Capsule stats for the team
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SandboxStats"
|
||||
$ref: "#/components/schemas/CapsuleStats"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
|
||||
/v1/sandboxes/{id}:
|
||||
/v1/capsules/{id}:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -648,36 +648,36 @@ paths:
|
||||
type: string
|
||||
|
||||
get:
|
||||
summary: Get sandbox details
|
||||
operationId: getSandbox
|
||||
tags: [sandboxes]
|
||||
summary: Get capsule details
|
||||
operationId: getCapsule
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Sandbox details
|
||||
description: Capsule details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Sandbox"
|
||||
$ref: "#/components/schemas/Capsule"
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
delete:
|
||||
summary: Destroy a sandbox
|
||||
operationId: destroySandbox
|
||||
tags: [sandboxes]
|
||||
summary: Destroy a capsule
|
||||
operationId: destroyCapsule
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: Sandbox destroyed
|
||||
description: Capsule destroyed
|
||||
|
||||
/v1/sandboxes/{id}/exec:
|
||||
/v1/capsules/{id}/exec:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -688,7 +688,7 @@ paths:
|
||||
post:
|
||||
summary: Execute a command
|
||||
operationId: execCommand
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
requestBody:
|
||||
@ -705,19 +705,19 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ExecResponse"
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/ping:
|
||||
/v1/capsules/{id}/ping:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -726,32 +726,32 @@ paths:
|
||||
type: string
|
||||
|
||||
post:
|
||||
summary: Reset sandbox inactivity timer
|
||||
operationId: pingSandbox
|
||||
tags: [sandboxes]
|
||||
summary: Reset capsule inactivity timer
|
||||
operationId: pingCapsule
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
description: |
|
||||
Resets the last_active_at timestamp for a running sandbox, preventing
|
||||
the auto-pause TTL from expiring. Use this as a keepalive for sandboxes
|
||||
Resets the last_active_at timestamp for a running capsule, preventing
|
||||
the auto-pause TTL from expiring. Use this as a keepalive for capsulees
|
||||
that are idle but should remain running.
|
||||
responses:
|
||||
"204":
|
||||
description: Ping acknowledged, inactivity timer reset
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/metrics:
|
||||
/v1/capsules/{id}/metrics:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -760,22 +760,22 @@ paths:
|
||||
type: string
|
||||
|
||||
get:
|
||||
summary: Get per-sandbox resource metrics
|
||||
operationId: getSandboxMetrics
|
||||
tags: [sandboxes]
|
||||
summary: Get per-capsule resource metrics
|
||||
operationId: getCapsuleMetrics
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- bearerAuth: []
|
||||
description: |
|
||||
Returns time-series CPU, memory, and disk metrics for a sandbox.
|
||||
Returns time-series CPU, memory, and disk metrics for a capsule.
|
||||
Three tiers are available with different granularity and retention:
|
||||
- `10m`: 500ms samples, last 10 minutes
|
||||
- `2h`: 30-second averages, last 2 hours
|
||||
- `24h`: 5-minute averages, last 24 hours
|
||||
|
||||
For running sandboxes, data comes from the host agent's in-memory
|
||||
ring buffer. For paused sandboxes, data is read from persisted
|
||||
snapshots in the database. Stopped/destroyed sandboxes return 404.
|
||||
For running capsulees, data comes from the host agent's in-memory
|
||||
ring buffer. For paused capsulees, data is read from persisted
|
||||
snapshots in the database. Stopped/destroyed capsulees return 404.
|
||||
parameters:
|
||||
- name: range
|
||||
in: query
|
||||
@ -791,7 +791,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SandboxMetrics"
|
||||
$ref: "#/components/schemas/CapsuleMetrics"
|
||||
"400":
|
||||
description: Invalid range parameter
|
||||
content:
|
||||
@ -799,13 +799,13 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"404":
|
||||
description: Sandbox not found or metrics not available
|
||||
description: Capsule not found or metrics not available
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/pause:
|
||||
/v1/capsules/{id}/pause:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -814,30 +814,30 @@ paths:
|
||||
type: string
|
||||
|
||||
post:
|
||||
summary: Pause a running sandbox
|
||||
operationId: pauseSandbox
|
||||
tags: [sandboxes]
|
||||
summary: Pause a running capsule
|
||||
operationId: pauseCapsule
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
description: |
|
||||
Takes a snapshot of the sandbox (VM state + memory + rootfs), then
|
||||
destroys all running resources. The sandbox exists only as files on
|
||||
Takes a snapshot of the capsule (VM state + memory + rootfs), then
|
||||
destroys all running resources. The capsule exists only as files on
|
||||
disk and can be resumed later.
|
||||
responses:
|
||||
"200":
|
||||
description: Sandbox paused (snapshot taken, resources released)
|
||||
description: Capsule paused (snapshot taken, resources released)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Sandbox"
|
||||
$ref: "#/components/schemas/Capsule"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/resume:
|
||||
/v1/capsules/{id}/resume:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -846,24 +846,24 @@ paths:
|
||||
type: string
|
||||
|
||||
post:
|
||||
summary: Resume a paused sandbox
|
||||
operationId: resumeSandbox
|
||||
tags: [sandboxes]
|
||||
summary: Resume a paused capsule
|
||||
operationId: resumeCapsule
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
description: |
|
||||
Restores a paused sandbox from its snapshot using UFFD for lazy
|
||||
Restores a paused capsule from its snapshot using UFFD for lazy
|
||||
memory loading. Boots a fresh Firecracker process, sets up a new
|
||||
network slot, and waits for envd to become ready.
|
||||
responses:
|
||||
"200":
|
||||
description: Sandbox resumed (new VM booted from snapshot)
|
||||
description: Capsule resumed (new VM booted from snapshot)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Sandbox"
|
||||
$ref: "#/components/schemas/Capsule"
|
||||
"409":
|
||||
description: Sandbox not paused
|
||||
description: Capsule not paused
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@ -877,9 +877,9 @@ paths:
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
description: |
|
||||
Pauses a running sandbox, takes a full snapshot, copies the snapshot
|
||||
Pauses a running capsule, takes a full snapshot, copies the snapshot
|
||||
files to the images directory as a reusable template, then destroys
|
||||
the sandbox. The template can be used to create new sandboxes.
|
||||
the capsule. The template can be used to create new capsulees.
|
||||
parameters:
|
||||
- name: overwrite
|
||||
in: query
|
||||
@ -902,7 +902,7 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Template"
|
||||
"409":
|
||||
description: Name already exists or sandbox not running
|
||||
description: Name already exists or capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@ -957,7 +957,7 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/files/write:
|
||||
/v1/capsules/{id}/files/write:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -968,7 +968,7 @@ paths:
|
||||
post:
|
||||
summary: Upload a file
|
||||
operationId: uploadFile
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
requestBody:
|
||||
@ -981,7 +981,7 @@ paths:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Absolute destination path inside the sandbox
|
||||
description: Absolute destination path inside the capsule
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
@ -990,7 +990,7 @@ paths:
|
||||
"204":
|
||||
description: File uploaded
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@ -1002,7 +1002,7 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/files/read:
|
||||
/v1/capsules/{id}/files/read:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -1013,7 +1013,7 @@ paths:
|
||||
post:
|
||||
summary: Download a file
|
||||
operationId: downloadFile
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
requestBody:
|
||||
@ -1031,13 +1031,13 @@ paths:
|
||||
type: string
|
||||
format: binary
|
||||
"404":
|
||||
description: Sandbox or file not found
|
||||
description: Capsule or file not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/files/list:
|
||||
/v1/capsules/{id}/files/list:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -1048,7 +1048,7 @@ paths:
|
||||
post:
|
||||
summary: List directory contents
|
||||
operationId: listDir
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
requestBody:
|
||||
@ -1065,19 +1065,19 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ListDirResponse"
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/files/mkdir:
|
||||
/v1/capsules/{id}/files/mkdir:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -1088,7 +1088,7 @@ paths:
|
||||
post:
|
||||
summary: Create a directory
|
||||
operationId: makeDir
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
requestBody:
|
||||
@ -1105,19 +1105,19 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MakeDirResponse"
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/files/remove:
|
||||
/v1/capsules/{id}/files/remove:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -1128,7 +1128,7 @@ paths:
|
||||
post:
|
||||
summary: Remove a file or directory
|
||||
operationId: removePath
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
requestBody:
|
||||
@ -1141,19 +1141,19 @@ paths:
|
||||
"204":
|
||||
description: File or directory removed
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/exec/stream:
|
||||
/v1/capsules/{id}/exec/stream:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -1164,7 +1164,7 @@ paths:
|
||||
get:
|
||||
summary: Stream command execution via WebSocket
|
||||
operationId: execStream
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
description: |
|
||||
@ -1194,19 +1194,19 @@ paths:
|
||||
"101":
|
||||
description: WebSocket upgrade
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/pty:
|
||||
/v1/capsules/{id}/pty:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -1217,7 +1217,7 @@ paths:
|
||||
get:
|
||||
summary: Interactive PTY session via WebSocket
|
||||
operationId: ptySession
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
description: |
|
||||
@ -1266,25 +1266,25 @@ paths:
|
||||
|
||||
Sessions have a 120-second inactivity timeout (reset on input/resize).
|
||||
Sessions persist across WebSocket disconnections — the process keeps
|
||||
running in the sandbox. Use the `tag` from the "started" response to
|
||||
running in the capsule. Use the `tag` from the "started" response to
|
||||
reconnect later.
|
||||
responses:
|
||||
"101":
|
||||
description: WebSocket upgrade
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/files/stream/write:
|
||||
/v1/capsules/{id}/files/stream/write:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -1295,11 +1295,11 @@ paths:
|
||||
post:
|
||||
summary: Upload a file (streaming)
|
||||
operationId: streamUploadFile
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
description: |
|
||||
Streams file content to the sandbox without buffering in memory.
|
||||
Streams file content to the capsule without buffering in memory.
|
||||
Suitable for large files. Uses the same multipart/form-data format
|
||||
as the non-streaming upload endpoint.
|
||||
requestBody:
|
||||
@ -1312,7 +1312,7 @@ paths:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Absolute destination path inside the sandbox
|
||||
description: Absolute destination path inside the capsule
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
@ -1321,19 +1321,19 @@ paths:
|
||||
"204":
|
||||
description: File uploaded
|
||||
"404":
|
||||
description: Sandbox not found
|
||||
description: Capsule not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/sandboxes/{id}/files/stream/read:
|
||||
/v1/capsules/{id}/files/stream/read:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
@ -1344,11 +1344,11 @@ paths:
|
||||
post:
|
||||
summary: Download a file (streaming)
|
||||
operationId: streamDownloadFile
|
||||
tags: [sandboxes]
|
||||
tags: [capsules]
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
description: |
|
||||
Streams file content from the sandbox without buffering in memory.
|
||||
Streams file content from the capsule without buffering in memory.
|
||||
Suitable for large files. Returns raw bytes with chunked transfer encoding.
|
||||
requestBody:
|
||||
required: true
|
||||
@ -1365,13 +1365,13 @@ paths:
|
||||
type: string
|
||||
format: binary
|
||||
"404":
|
||||
description: Sandbox or file not found
|
||||
description: Capsule or file not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Sandbox not running
|
||||
description: Capsule not running
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@ -1469,14 +1469,14 @@ paths:
|
||||
description: |
|
||||
Admins can delete any host. Team owners and admins can delete BYOC hosts
|
||||
belonging to their team. Without `?force=true`, returns 409 if the host
|
||||
has active sandboxes. With `?force=true`, destroys all sandboxes first.
|
||||
has active capsulees. With `?force=true`, destroys all capsulees first.
|
||||
parameters:
|
||||
- name: force
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
description: If true, destroy all sandboxes on the host before deleting.
|
||||
description: If true, destroy all capsulees on the host before deleting.
|
||||
responses:
|
||||
"204":
|
||||
description: Host deleted
|
||||
@ -1487,11 +1487,11 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: Host has active sandboxes (only when force is not set)
|
||||
description: Host has active capsulees (only when force is not set)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HostHasSandboxesError"
|
||||
$ref: "#/components/schemas/HostHasCapsulesError"
|
||||
|
||||
/v1/hosts/{id}/token:
|
||||
parameters:
|
||||
@ -1644,7 +1644,7 @@ paths:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
description: |
|
||||
Returns the list of sandbox IDs that would be destroyed if the host
|
||||
Returns the list of capsule IDs that would be destroyed if the host
|
||||
were deleted with `?force=true`. No state is modified.
|
||||
responses:
|
||||
"200":
|
||||
@ -1917,7 +1917,7 @@ components:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
description: API key for sandbox lifecycle operations. Create via POST /v1/api-keys.
|
||||
description: API key for capsule lifecycle operations. Create via POST /v1/api-keys.
|
||||
|
||||
bearerAuth:
|
||||
type: http
|
||||
@ -2002,7 +2002,7 @@ components:
|
||||
description: Full plaintext key. Only returned on creation, never again.
|
||||
nullable: true
|
||||
|
||||
CreateSandboxRequest:
|
||||
CreateCapsuleRequest:
|
||||
type: object
|
||||
properties:
|
||||
template:
|
||||
@ -2018,11 +2018,11 @@ components:
|
||||
type: integer
|
||||
default: 0
|
||||
description: >
|
||||
Auto-pause TTL in seconds. The sandbox is automatically paused
|
||||
Auto-pause TTL in seconds. The capsule is automatically paused
|
||||
after this duration of inactivity (no exec or ping). 0 means
|
||||
no auto-pause.
|
||||
|
||||
SandboxStats:
|
||||
CapsuleStats:
|
||||
type: object
|
||||
properties:
|
||||
range:
|
||||
@ -2073,7 +2073,7 @@ components:
|
||||
items:
|
||||
type: integer
|
||||
|
||||
Sandbox:
|
||||
Capsule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
@ -2114,7 +2114,7 @@ components:
|
||||
properties:
|
||||
sandbox_id:
|
||||
type: string
|
||||
description: ID of the running sandbox to snapshot.
|
||||
description: ID of the running capsule to snapshot.
|
||||
name:
|
||||
type: string
|
||||
description: Name for the snapshot template. Auto-generated if omitted.
|
||||
@ -2180,7 +2180,7 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Absolute file path inside the sandbox
|
||||
description: Absolute file path inside the capsule
|
||||
|
||||
ListDirRequest:
|
||||
type: object
|
||||
@ -2188,7 +2188,7 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Directory path inside the sandbox
|
||||
description: Directory path inside the capsule
|
||||
depth:
|
||||
type: integer
|
||||
default: 1
|
||||
@ -2238,7 +2238,7 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Directory path to create inside the sandbox
|
||||
description: Directory path to create inside the capsule
|
||||
|
||||
MakeDirResponse:
|
||||
type: object
|
||||
@ -2252,7 +2252,7 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Path to remove inside the sandbox
|
||||
description: Path to remove inside the capsule
|
||||
|
||||
CreateHostRequest:
|
||||
type: object
|
||||
@ -2390,9 +2390,9 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: IDs of sandboxes that would be destroyed on force-delete.
|
||||
description: IDs of capsulees that would be destroyed on force-delete.
|
||||
|
||||
HostHasSandboxesError:
|
||||
HostHasCapsulesError:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
@ -2407,7 +2407,7 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: IDs of active sandboxes blocking deletion.
|
||||
description: IDs of active capsulees blocking deletion.
|
||||
|
||||
AddTagRequest:
|
||||
type: object
|
||||
@ -2471,7 +2471,7 @@ components:
|
||||
items:
|
||||
$ref: "#/components/schemas/TeamMember"
|
||||
|
||||
SandboxMetrics:
|
||||
CapsuleMetrics:
|
||||
type: object
|
||||
properties:
|
||||
sandbox_id:
|
||||
|
||||
@ -116,8 +116,8 @@ func New(
|
||||
// JWT-authenticated: user search (for add-member UI).
|
||||
r.With(requireJWT(jwtSecret)).Get("/v1/users/search", usersH.Search)
|
||||
|
||||
// Sandbox lifecycle: accepts API key or JWT bearer token.
|
||||
r.Route("/v1/sandboxes", func(r chi.Router) {
|
||||
// Capsule lifecycle: accepts API key or JWT bearer token.
|
||||
r.Route("/v1/capsules", func(r chi.Router) {
|
||||
r.Use(requireAPIKeyOrJWT(queries, jwtSecret))
|
||||
r.Post("/", sandbox.Create)
|
||||
r.Get("/", sandbox.List)
|
||||
@ -229,7 +229,7 @@ func serveDocs(w http.ResponseWriter, r *http.Request) {
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wrenn Sandbox API</title>
|
||||
<title>Wrenn API</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui.css" integrity="sha384-rcbEi6xgdPk0iWkAQzT2F3FeBJXdG+ydrawGlfHAFIZG7wU6aKbQaRewysYpmrlW" crossorigin="anonymous">
|
||||
<style>
|
||||
body { margin: 0; background: #fafafa; }
|
||||
|
||||
Reference in New Issue
Block a user