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.
|
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".
|
- **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)
|
- **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).
|
- **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
|
### Key Request Flows
|
||||||
|
|
||||||
**Sandbox creation** (`POST /v1/sandboxes`):
|
**Sandbox creation** (`POST /v1/capsules`):
|
||||||
1. API handler generates sandbox ID, inserts into DB as "pending"
|
1. API handler generates sandbox ID, inserts into DB as "pending"
|
||||||
2. RPC `CreateSandbox` → host agent → `sandbox.Manager.Create()`
|
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
|
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
|
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
|
1. API handler verifies sandbox is "running" in DB
|
||||||
2. RPC `Exec` → host agent → `sandbox.Manager.Exec()` → `envdclient.Exec()`
|
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
|
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
|
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
|
1. WebSocket upgrade, read first message for cmd/args
|
||||||
2. RPC `ExecStream` → host agent → `sandbox.Manager.ExecStream()` → `envdclient.ExecStream()`
|
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
|
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[]>> {
|
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>> {
|
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 = {
|
export type CreateCapsuleParams = {
|
||||||
@ -32,19 +32,19 @@ export type CreateCapsuleParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function createCapsule(params: CreateCapsuleParams): Promise<ApiResult<Capsule>> {
|
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>> {
|
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>> {
|
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>> {
|
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 = {
|
export type Snapshot = {
|
||||||
@ -57,8 +57,8 @@ export type Snapshot = {
|
|||||||
platform: boolean;
|
platform: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function createSnapshot(sandboxId: string, name?: string): Promise<ApiResult<Snapshot>> {
|
export async function createSnapshot(capsuleId: string, name?: string): Promise<ApiResult<Snapshot>> {
|
||||||
return apiFetch('POST', '/api/v1/snapshots', { sandbox_id: sandboxId, name });
|
return apiFetch('POST', '/api/v1/snapshots', { sandbox_id: capsuleId, name });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listSnapshots(typeFilter?: string): Promise<ApiResult<Snapshot[]>> {
|
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]}`;
|
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 {
|
try {
|
||||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
|
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',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ path, depth }),
|
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 {
|
try {
|
||||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
|
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',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ path }),
|
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' };
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
|
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',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ path }),
|
body: JSON.stringify({ path }),
|
||||||
|
|||||||
@ -15,8 +15,8 @@ export type MetricsResponse = {
|
|||||||
points: MetricPoint[];
|
points: MetricPoint[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchSandboxMetrics(id: string, range: MetricRange): Promise<ApiResult<MetricsResponse>> {
|
export async function fetchCapsuleMetrics(id: string, range: MetricRange): Promise<ApiResult<MetricsResponse>> {
|
||||||
return apiFetch('GET', `/api/v1/sandboxes/${id}/metrics?range=${range}`);
|
return apiFetch('GET', `/api/v1/capsules/${id}/metrics?range=${range}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const METRIC_RANGES: MetricRange[] = ['5m', '10m', '1h', '6h', '24h'];
|
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>> {
|
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> = {
|
export const POLL_INTERVALS: Record<TimeRange, number> = {
|
||||||
|
|||||||
@ -11,11 +11,11 @@
|
|||||||
import { tokenize, type ThemedToken } from '$lib/highlight';
|
import { tokenize, type ThemedToken } from '$lib/highlight';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
sandboxId: string;
|
capsuleId: string;
|
||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { sandboxId, isRunning }: Props = $props();
|
let { capsuleId, isRunning }: Props = $props();
|
||||||
|
|
||||||
// Directory navigation state
|
// Directory navigation state
|
||||||
let currentPath = $state('~');
|
let currentPath = $state('~');
|
||||||
@ -124,7 +124,7 @@
|
|||||||
dirLoading = true;
|
dirLoading = true;
|
||||||
dirError = null;
|
dirError = null;
|
||||||
const gen = ++dirGeneration;
|
const gen = ++dirGeneration;
|
||||||
const result = await listDir(sandboxId, currentPath);
|
const result = await listDir(capsuleId, currentPath);
|
||||||
if (gen !== dirGeneration) return; // stale response
|
if (gen !== dirGeneration) return; // stale response
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
entries = result.data.entries ?? [];
|
entries = result.data.entries ?? [];
|
||||||
@ -159,7 +159,7 @@
|
|||||||
|
|
||||||
fileLoading = true;
|
fileLoading = true;
|
||||||
const gen = ++fileGeneration;
|
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 (gen !== fileGeneration) return; // stale response — user clicked another file
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
if (looksLikeBinary(result.data)) {
|
if (looksLikeBinary(result.data)) {
|
||||||
@ -197,7 +197,7 @@
|
|||||||
if (!selectedFile || downloading) return;
|
if (!selectedFile || downloading) return;
|
||||||
downloading = true;
|
downloading = true;
|
||||||
try {
|
try {
|
||||||
await downloadFile(sandboxId, selectedFile.path, selectedFile.name);
|
await downloadFile(capsuleId, selectedFile.path, selectedFile.name);
|
||||||
} catch {
|
} catch {
|
||||||
fileError = 'Download failed';
|
fileError = 'Download failed';
|
||||||
}
|
}
|
||||||
@ -214,7 +214,7 @@
|
|||||||
|
|
||||||
async function navigateOrOpenFile(path: string) {
|
async function navigateOrOpenFile(path: string) {
|
||||||
// First try as directory
|
// First try as directory
|
||||||
const dirResult = await listDir(sandboxId, path);
|
const dirResult = await listDir(capsuleId, path);
|
||||||
if (dirResult.ok) {
|
if (dirResult.ok) {
|
||||||
// Resolve actual path from entries (handles ~ expansion by envd)
|
// Resolve actual path from entries (handles ~ expansion by envd)
|
||||||
const resolvedEntries = dirResult.data.entries ?? [];
|
const resolvedEntries = dirResult.data.entries ?? [];
|
||||||
@ -245,7 +245,7 @@
|
|||||||
// Navigate to parent directory
|
// Navigate to parent directory
|
||||||
currentPath = parentPath;
|
currentPath = parentPath;
|
||||||
pathInput = parentPath;
|
pathInput = parentPath;
|
||||||
const parentResult = await listDir(sandboxId, parentPath);
|
const parentResult = await listDir(capsuleId, parentPath);
|
||||||
if (parentResult.ok) {
|
if (parentResult.ok) {
|
||||||
entries = parentResult.data.entries ?? [];
|
entries = parentResult.data.entries ?? [];
|
||||||
// Find the file in parent listing
|
// Find the file in parent listing
|
||||||
|
|||||||
@ -3,12 +3,12 @@
|
|||||||
import { auth } from '$lib/auth.svelte';
|
import { auth } from '$lib/auth.svelte';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
sandboxId: string;
|
capsuleId: string;
|
||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { sandboxId, isRunning, visible = true }: Props = $props();
|
let { capsuleId, isRunning, visible = true }: Props = $props();
|
||||||
|
|
||||||
type ConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
type ConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
|
||||||
|
|
||||||
@ -93,7 +93,7 @@
|
|||||||
function getWsUrl(): string {
|
function getWsUrl(): string {
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const token = auth.token ? `?token=${encodeURIComponent(auth.token)}` : '';
|
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) {
|
function wsSend(ws: WebSocket | null, data: string) {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ let loadingPromise: Promise<HighlighterGeneric<any, any>> | null = null;
|
|||||||
const THEME = 'vesper';
|
const THEME = 'vesper';
|
||||||
|
|
||||||
// Extensions → shiki language IDs.
|
// 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> = {
|
const EXT_TO_LANG: Record<string, string> = {
|
||||||
// Go
|
// Go
|
||||||
go: 'go', mod: 'go', sum: 'go',
|
go: 'go', mod: 'go', sum: 'go',
|
||||||
|
|||||||
@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
// Delete confirmation
|
// Delete confirmation
|
||||||
let deleteTarget = $state<Host | null>(null);
|
let deleteTarget = $state<Host | null>(null);
|
||||||
let deletePreviewSandboxes = $state<string[]>([]);
|
let deletePreviewCapsules = $state<string[]>([]);
|
||||||
let deletePreviewLoading = $state(false);
|
let deletePreviewLoading = $state(false);
|
||||||
let deleting = $state(false);
|
let deleting = $state(false);
|
||||||
let deleteError = $state<string | null>(null);
|
let deleteError = $state<string | null>(null);
|
||||||
@ -124,12 +124,12 @@
|
|||||||
async function openDeleteConfirm(host: Host) {
|
async function openDeleteConfirm(host: Host) {
|
||||||
deleteTarget = host;
|
deleteTarget = host;
|
||||||
deleteError = null;
|
deleteError = null;
|
||||||
deletePreviewSandboxes = [];
|
deletePreviewCapsules = [];
|
||||||
deletePreviewLoading = true;
|
deletePreviewLoading = true;
|
||||||
const preview = await getDeletePreview(host.id);
|
const preview = await getDeletePreview(host.id);
|
||||||
deletePreviewLoading = false;
|
deletePreviewLoading = false;
|
||||||
if (preview.ok) {
|
if (preview.ok) {
|
||||||
deletePreviewSandboxes = preview.data.sandbox_ids;
|
deletePreviewCapsules = preview.data.sandbox_ids;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +137,7 @@
|
|||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
deleting = true;
|
deleting = true;
|
||||||
deleteError = null;
|
deleteError = null;
|
||||||
const result = await deleteHost(deleteTarget.id, deletePreviewSandboxes.length > 0);
|
const result = await deleteHost(deleteTarget.id, deletePreviewCapsules.length > 0);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
allHosts = allHosts.filter((h) => h.id !== deleteTarget!.id);
|
allHosts = allHosts.filter((h) => h.id !== deleteTarget!.id);
|
||||||
deleteTarget = null;
|
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>
|
<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…
|
Checking active capsules…
|
||||||
</div>
|
</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">
|
<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)]">
|
<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>
|
||||||
<p class="mt-0.5 text-meta text-[var(--color-amber)]/70">
|
<p class="mt-0.5 text-meta text-[var(--color-amber)]/70">
|
||||||
All running workloads on this host will be terminated immediately.
|
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>
|
<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…
|
Deleting…
|
||||||
{:else}
|
{:else}
|
||||||
{deletePreviewSandboxes.length > 0 ? 'Force Delete' : 'Delete'}
|
{deletePreviewCapsules.length > 0 ? 'Force Delete' : 'Delete'}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,14 +9,14 @@
|
|||||||
import FilesTab from '$lib/components/FilesTab.svelte';
|
import FilesTab from '$lib/components/FilesTab.svelte';
|
||||||
import TerminalTab from '$lib/components/TerminalTab.svelte';
|
import TerminalTab from '$lib/components/TerminalTab.svelte';
|
||||||
import {
|
import {
|
||||||
fetchSandboxMetrics,
|
fetchCapsuleMetrics,
|
||||||
METRIC_RANGES,
|
METRIC_RANGES,
|
||||||
METRIC_POLL_INTERVALS,
|
METRIC_POLL_INTERVALS,
|
||||||
type MetricRange,
|
type MetricRange,
|
||||||
type MetricPoint
|
type MetricPoint
|
||||||
} from '$lib/api/metrics';
|
} from '$lib/api/metrics';
|
||||||
|
|
||||||
const sandboxId: string = $page.params.id ?? '';
|
const capsuleId: string = $page.params.id ?? '';
|
||||||
|
|
||||||
let capsule = $state<Capsule | null>(null);
|
let capsule = $state<Capsule | null>(null);
|
||||||
let capsuleLoading = $state(true);
|
let capsuleLoading = $state(true);
|
||||||
@ -96,7 +96,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function loadCapsule() {
|
async function loadCapsule() {
|
||||||
const result = await getCapsule(sandboxId);
|
const result = await getCapsule(capsuleId);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
capsule = result.data;
|
capsule = result.data;
|
||||||
capsuleError = null;
|
capsuleError = null;
|
||||||
@ -108,7 +108,7 @@
|
|||||||
|
|
||||||
async function loadMetrics() {
|
async function loadMetrics() {
|
||||||
if (!metricsAvailable) return;
|
if (!metricsAvailable) return;
|
||||||
const result = await fetchSandboxMetrics(sandboxId, range);
|
const result = await fetchCapsuleMetrics(capsuleId, range);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
points = result.data.points;
|
points = result.data.points;
|
||||||
metricsError = null;
|
metricsError = null;
|
||||||
@ -441,7 +441,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Wrenn — {sandboxId}</title>
|
<title>Wrenn — {capsuleId}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -575,11 +575,11 @@
|
|||||||
<!-- Tab content -->
|
<!-- Tab content -->
|
||||||
<!-- Terminal stays mounted so sessions survive tab switches -->
|
<!-- Terminal stays mounted so sessions survive tab switches -->
|
||||||
<div class="flex flex-1 min-h-0" style:display={activeTab === 'terminal' ? 'flex' : 'none'}>
|
<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>
|
</div>
|
||||||
{#if activeTab === 'files'}
|
{#if activeTab === 'files'}
|
||||||
<div class="anim-in flex flex-1 min-h-0" style="animation-delay: 0.05s">
|
<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>
|
</div>
|
||||||
{:else if activeTab === 'metrics'}
|
{:else if activeTab === 'metrics'}
|
||||||
<div
|
<div
|
||||||
@ -757,14 +757,14 @@
|
|||||||
|
|
||||||
<SnapshotDialog
|
<SnapshotDialog
|
||||||
open={showSnapshot}
|
open={showSnapshot}
|
||||||
capsuleId={sandboxId}
|
capsuleId={capsuleId}
|
||||||
onclose={() => { showSnapshot = false; }}
|
onclose={() => { showSnapshot = false; }}
|
||||||
onsnapshot={() => { goto('/dashboard/capsules'); }}
|
onsnapshot={() => { goto('/dashboard/capsules'); }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DestroyDialog
|
<DestroyDialog
|
||||||
open={showDestroy}
|
open={showDestroy}
|
||||||
capsuleId={sandboxId}
|
capsuleId={capsuleId}
|
||||||
onclose={() => { showDestroy = false; }}
|
onclose={() => { showDestroy = false; }}
|
||||||
ondestroyed={() => { goto('/dashboard/capsules'); }}
|
ondestroyed={() => { goto('/dashboard/capsules'); }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
// Delete confirmation
|
// Delete confirmation
|
||||||
let deleteTarget = $state<Host | null>(null);
|
let deleteTarget = $state<Host | null>(null);
|
||||||
let deletePreviewSandboxes = $state<string[]>([]);
|
let deletePreviewCapsules = $state<string[]>([]);
|
||||||
let deletePreviewLoading = $state(false);
|
let deletePreviewLoading = $state(false);
|
||||||
let deleting = $state(false);
|
let deleting = $state(false);
|
||||||
let deleteError = $state<string | null>(null);
|
let deleteError = $state<string | null>(null);
|
||||||
@ -97,12 +97,12 @@
|
|||||||
async function openDeleteConfirm(host: Host) {
|
async function openDeleteConfirm(host: Host) {
|
||||||
deleteTarget = host;
|
deleteTarget = host;
|
||||||
deleteError = null;
|
deleteError = null;
|
||||||
deletePreviewSandboxes = [];
|
deletePreviewCapsules = [];
|
||||||
deletePreviewLoading = true;
|
deletePreviewLoading = true;
|
||||||
const preview = await getDeletePreview(host.id);
|
const preview = await getDeletePreview(host.id);
|
||||||
deletePreviewLoading = false;
|
deletePreviewLoading = false;
|
||||||
if (preview.ok) {
|
if (preview.ok) {
|
||||||
deletePreviewSandboxes = preview.data.sandbox_ids;
|
deletePreviewCapsules = preview.data.sandbox_ids;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +110,7 @@
|
|||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
deleting = true;
|
deleting = true;
|
||||||
deleteError = null;
|
deleteError = null;
|
||||||
const result = await deleteHost(deleteTarget.id, deletePreviewSandboxes.length > 0);
|
const result = await deleteHost(deleteTarget.id, deletePreviewCapsules.length > 0);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
hosts = hosts.filter((h) => h.id !== deleteTarget!.id);
|
hosts = hosts.filter((h) => h.id !== deleteTarget!.id);
|
||||||
deleteTarget = null;
|
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>
|
<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…
|
Checking active capsules…
|
||||||
</div>
|
</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">
|
<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)]">
|
<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>
|
||||||
<p class="mt-0.5 text-meta text-[var(--color-amber)]/70">
|
<p class="mt-0.5 text-meta text-[var(--color-amber)]/70">
|
||||||
All running workloads on this host will be terminated immediately.
|
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>
|
<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…
|
Deleting…
|
||||||
{:else}
|
{:else}
|
||||||
{deletePreviewSandboxes.length > 0 ? 'Force Delete' : 'Delete Host'}
|
{deletePreviewCapsules.length > 0 ? 'Force Delete' : 'Delete Host'}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -45,7 +45,7 @@ type execResponse struct {
|
|||||||
Encoding string `json:"encoding"`
|
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) {
|
func (h *execHandler) Exec(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|||||||
@ -47,7 +47,7 @@ type wsOutMsg struct {
|
|||||||
ExitCode *int32 `json:"exit_code,omitempty"` // only for "exit"
|
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) {
|
func (h *execStreamHandler) ExecStream(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|||||||
@ -25,7 +25,7 @@ func newFilesHandler(db *db.Queries, pool *lifecycle.HostClientPool) *filesHandl
|
|||||||
return &filesHandler{db: db, pool: pool}
|
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:
|
// Expects multipart/form-data with:
|
||||||
// - "path" text field: absolute destination path inside the sandbox
|
// - "path" text field: absolute destination path inside the sandbox
|
||||||
// - "file" file field: binary content to write
|
// - "file" file field: binary content to write
|
||||||
@ -105,7 +105,7 @@ type readFileRequest struct {
|
|||||||
Path string `json:"path"`
|
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.
|
// Accepts JSON body with path, returns raw file content with Content-Disposition.
|
||||||
func (h *filesHandler) Download(w http.ResponseWriter, r *http.Request) {
|
func (h *filesHandler) Download(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
|
|||||||
@ -26,7 +26,7 @@ func newFilesStreamHandler(db *db.Queries, pool *lifecycle.HostClientPool) *file
|
|||||||
return &filesStreamHandler{db: db, pool: pool}
|
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.
|
// 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.
|
// Streams file content directly from the request body to the host agent without buffering.
|
||||||
func (h *filesStreamHandler) StreamUpload(w http.ResponseWriter, r *http.Request) {
|
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)
|
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.
|
// Accepts JSON body with path, streams file content back without buffering.
|
||||||
func (h *filesStreamHandler) StreamDownload(w http.ResponseWriter, r *http.Request) {
|
func (h *filesStreamHandler) StreamDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
|
|||||||
@ -56,7 +56,7 @@ type removeRequest struct {
|
|||||||
Path string `json:"path"`
|
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) {
|
func (h *fsHandler) ListDir(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@ -113,7 +113,7 @@ func (h *fsHandler) ListDir(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, listDirResponse{Entries: entries})
|
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) {
|
func (h *fsHandler) MakeDir(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ctx := r.Context()
|
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)})
|
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) {
|
func (h *fsHandler) Remove(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|||||||
@ -38,7 +38,7 @@ type metricsResponse struct {
|
|||||||
Points []metricPointResponse `json:"points"`
|
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) {
|
func (h *sandboxMetricsHandler) GetMetrics(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ctx := r.Context()
|
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) {
|
func (h *ptyHandler) PtySession(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|||||||
@ -73,7 +73,7 @@ func sandboxToResponse(sb db.Sandbox) sandboxResponse {
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create handles POST /v1/sandboxes.
|
// Create handles POST /v1/capsules.
|
||||||
func (h *sandboxHandler) Create(w http.ResponseWriter, r *http.Request) {
|
func (h *sandboxHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||||
var req createSandboxRequest
|
var req createSandboxRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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))
|
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) {
|
func (h *sandboxHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||||
ac := auth.MustFromContext(r.Context())
|
ac := auth.MustFromContext(r.Context())
|
||||||
sandboxes, err := h.svc.List(r.Context(), ac.TeamID)
|
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)
|
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) {
|
func (h *sandboxHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ac := auth.MustFromContext(r.Context())
|
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))
|
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) {
|
func (h *sandboxHandler) Pause(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ac := auth.MustFromContext(r.Context())
|
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))
|
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) {
|
func (h *sandboxHandler) Resume(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ac := auth.MustFromContext(r.Context())
|
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))
|
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) {
|
func (h *sandboxHandler) Ping(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ac := auth.MustFromContext(r.Context())
|
ac := auth.MustFromContext(r.Context())
|
||||||
@ -205,7 +205,7 @@ func (h *sandboxHandler) Ping(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
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) {
|
func (h *sandboxHandler) Destroy(w http.ResponseWriter, r *http.Request) {
|
||||||
sandboxIDStr := chi.URLParam(r, "id")
|
sandboxIDStr := chi.URLParam(r, "id")
|
||||||
ac := auth.MustFromContext(r.Context())
|
ac := auth.MustFromContext(r.Context())
|
||||||
|
|||||||
@ -43,7 +43,7 @@ type statsResponse struct {
|
|||||||
Series statsSeriesResponse `json:"series"`
|
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) {
|
func (h *statsHandler) GetStats(w http.ResponseWriter, r *http.Request) {
|
||||||
ac := auth.MustFromContext(r.Context())
|
ac := auth.MustFromContext(r.Context())
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
openapi: "3.1.0"
|
openapi: "3.1.0"
|
||||||
info:
|
info:
|
||||||
title: Wrenn Sandbox API
|
title: Wrenn API
|
||||||
description: MicroVM-based code execution platform API.
|
description: MicroVM-based code execution platform API.
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
|
|
||||||
@ -393,7 +393,7 @@ paths:
|
|||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
description: |
|
description: |
|
||||||
Owner only. Soft-deletes the team and destroys all running/paused/starting
|
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:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
description: Team deleted
|
description: Team deleted
|
||||||
@ -570,11 +570,11 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes:
|
/v1/capsules:
|
||||||
post:
|
post:
|
||||||
summary: Create a sandbox
|
summary: Create a capsule
|
||||||
operationId: createSandbox
|
operationId: createCapsule
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -582,14 +582,14 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/CreateSandboxRequest"
|
$ref: "#/components/schemas/CreateCapsuleRequest"
|
||||||
responses:
|
responses:
|
||||||
"201":
|
"201":
|
||||||
description: Sandbox created
|
description: Capsule created
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Sandbox"
|
$ref: "#/components/schemas/Capsule"
|
||||||
"502":
|
"502":
|
||||||
description: Host agent error
|
description: Host agent error
|
||||||
content:
|
content:
|
||||||
@ -598,26 +598,26 @@ paths:
|
|||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
get:
|
get:
|
||||||
summary: List sandboxes for your team
|
summary: List capsulees for your team
|
||||||
operationId: listSandboxes
|
operationId: listCapsules
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: List of sandboxes
|
description: List of capsulees
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Sandbox"
|
$ref: "#/components/schemas/Capsule"
|
||||||
|
|
||||||
/v1/sandboxes/stats:
|
/v1/capsules/stats:
|
||||||
get:
|
get:
|
||||||
summary: Get sandbox usage stats for your team
|
summary: Get capsule usage stats for your team
|
||||||
operationId: getSandboxStats
|
operationId: getCapsuleStats
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -631,15 +631,15 @@ paths:
|
|||||||
description: Time window for the time-series data.
|
description: Time window for the time-series data.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Sandbox stats for the team
|
description: Capsule stats for the team
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/SandboxStats"
|
$ref: "#/components/schemas/CapsuleStats"
|
||||||
"400":
|
"400":
|
||||||
$ref: "#/components/responses/BadRequest"
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
|
||||||
/v1/sandboxes/{id}:
|
/v1/capsules/{id}:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -648,36 +648,36 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
get:
|
get:
|
||||||
summary: Get sandbox details
|
summary: Get capsule details
|
||||||
operationId: getSandbox
|
operationId: getCapsule
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Sandbox details
|
description: Capsule details
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Sandbox"
|
$ref: "#/components/schemas/Capsule"
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
summary: Destroy a sandbox
|
summary: Destroy a capsule
|
||||||
operationId: destroySandbox
|
operationId: destroyCapsule
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
responses:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
description: Sandbox destroyed
|
description: Capsule destroyed
|
||||||
|
|
||||||
/v1/sandboxes/{id}/exec:
|
/v1/capsules/{id}/exec:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -688,7 +688,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: Execute a command
|
summary: Execute a command
|
||||||
operationId: execCommand
|
operationId: execCommand
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -705,19 +705,19 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ExecResponse"
|
$ref: "#/components/schemas/ExecResponse"
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/ping:
|
/v1/capsules/{id}/ping:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -726,32 +726,32 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
post:
|
post:
|
||||||
summary: Reset sandbox inactivity timer
|
summary: Reset capsule inactivity timer
|
||||||
operationId: pingSandbox
|
operationId: pingCapsule
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
description: |
|
description: |
|
||||||
Resets the last_active_at timestamp for a running sandbox, preventing
|
Resets the last_active_at timestamp for a running capsule, preventing
|
||||||
the auto-pause TTL from expiring. Use this as a keepalive for sandboxes
|
the auto-pause TTL from expiring. Use this as a keepalive for capsulees
|
||||||
that are idle but should remain running.
|
that are idle but should remain running.
|
||||||
responses:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
description: Ping acknowledged, inactivity timer reset
|
description: Ping acknowledged, inactivity timer reset
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/metrics:
|
/v1/capsules/{id}/metrics:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -760,22 +760,22 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
get:
|
get:
|
||||||
summary: Get per-sandbox resource metrics
|
summary: Get per-capsule resource metrics
|
||||||
operationId: getSandboxMetrics
|
operationId: getCapsuleMetrics
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
description: |
|
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:
|
Three tiers are available with different granularity and retention:
|
||||||
- `10m`: 500ms samples, last 10 minutes
|
- `10m`: 500ms samples, last 10 minutes
|
||||||
- `2h`: 30-second averages, last 2 hours
|
- `2h`: 30-second averages, last 2 hours
|
||||||
- `24h`: 5-minute averages, last 24 hours
|
- `24h`: 5-minute averages, last 24 hours
|
||||||
|
|
||||||
For running sandboxes, data comes from the host agent's in-memory
|
For running capsulees, data comes from the host agent's in-memory
|
||||||
ring buffer. For paused sandboxes, data is read from persisted
|
ring buffer. For paused capsulees, data is read from persisted
|
||||||
snapshots in the database. Stopped/destroyed sandboxes return 404.
|
snapshots in the database. Stopped/destroyed capsulees return 404.
|
||||||
parameters:
|
parameters:
|
||||||
- name: range
|
- name: range
|
||||||
in: query
|
in: query
|
||||||
@ -791,7 +791,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/SandboxMetrics"
|
$ref: "#/components/schemas/CapsuleMetrics"
|
||||||
"400":
|
"400":
|
||||||
description: Invalid range parameter
|
description: Invalid range parameter
|
||||||
content:
|
content:
|
||||||
@ -799,13 +799,13 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found or metrics not available
|
description: Capsule not found or metrics not available
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/pause:
|
/v1/capsules/{id}/pause:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -814,30 +814,30 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
post:
|
post:
|
||||||
summary: Pause a running sandbox
|
summary: Pause a running capsule
|
||||||
operationId: pauseSandbox
|
operationId: pauseCapsule
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
description: |
|
description: |
|
||||||
Takes a snapshot of the sandbox (VM state + memory + rootfs), then
|
Takes a snapshot of the capsule (VM state + memory + rootfs), then
|
||||||
destroys all running resources. The sandbox exists only as files on
|
destroys all running resources. The capsule exists only as files on
|
||||||
disk and can be resumed later.
|
disk and can be resumed later.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Sandbox paused (snapshot taken, resources released)
|
description: Capsule paused (snapshot taken, resources released)
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Sandbox"
|
$ref: "#/components/schemas/Capsule"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/resume:
|
/v1/capsules/{id}/resume:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -846,24 +846,24 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
post:
|
post:
|
||||||
summary: Resume a paused sandbox
|
summary: Resume a paused capsule
|
||||||
operationId: resumeSandbox
|
operationId: resumeCapsule
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
description: |
|
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
|
memory loading. Boots a fresh Firecracker process, sets up a new
|
||||||
network slot, and waits for envd to become ready.
|
network slot, and waits for envd to become ready.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Sandbox resumed (new VM booted from snapshot)
|
description: Capsule resumed (new VM booted from snapshot)
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Sandbox"
|
$ref: "#/components/schemas/Capsule"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not paused
|
description: Capsule not paused
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@ -877,9 +877,9 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
description: |
|
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
|
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:
|
parameters:
|
||||||
- name: overwrite
|
- name: overwrite
|
||||||
in: query
|
in: query
|
||||||
@ -902,7 +902,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Template"
|
$ref: "#/components/schemas/Template"
|
||||||
"409":
|
"409":
|
||||||
description: Name already exists or sandbox not running
|
description: Name already exists or capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@ -957,7 +957,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/files/write:
|
/v1/capsules/{id}/files/write:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -968,7 +968,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: Upload a file
|
summary: Upload a file
|
||||||
operationId: uploadFile
|
operationId: uploadFile
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -981,7 +981,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
description: Absolute destination path inside the sandbox
|
description: Absolute destination path inside the capsule
|
||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
@ -990,7 +990,7 @@ paths:
|
|||||||
"204":
|
"204":
|
||||||
description: File uploaded
|
description: File uploaded
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@ -1002,7 +1002,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/files/read:
|
/v1/capsules/{id}/files/read:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -1013,7 +1013,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: Download a file
|
summary: Download a file
|
||||||
operationId: downloadFile
|
operationId: downloadFile
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -1031,13 +1031,13 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox or file not found
|
description: Capsule or file not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/files/list:
|
/v1/capsules/{id}/files/list:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -1048,7 +1048,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: List directory contents
|
summary: List directory contents
|
||||||
operationId: listDir
|
operationId: listDir
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -1065,19 +1065,19 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ListDirResponse"
|
$ref: "#/components/schemas/ListDirResponse"
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/files/mkdir:
|
/v1/capsules/{id}/files/mkdir:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -1088,7 +1088,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: Create a directory
|
summary: Create a directory
|
||||||
operationId: makeDir
|
operationId: makeDir
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -1105,19 +1105,19 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/MakeDirResponse"
|
$ref: "#/components/schemas/MakeDirResponse"
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/files/remove:
|
/v1/capsules/{id}/files/remove:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -1128,7 +1128,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: Remove a file or directory
|
summary: Remove a file or directory
|
||||||
operationId: removePath
|
operationId: removePath
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -1141,19 +1141,19 @@ paths:
|
|||||||
"204":
|
"204":
|
||||||
description: File or directory removed
|
description: File or directory removed
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/exec/stream:
|
/v1/capsules/{id}/exec/stream:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -1164,7 +1164,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: Stream command execution via WebSocket
|
summary: Stream command execution via WebSocket
|
||||||
operationId: execStream
|
operationId: execStream
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
description: |
|
description: |
|
||||||
@ -1194,19 +1194,19 @@ paths:
|
|||||||
"101":
|
"101":
|
||||||
description: WebSocket upgrade
|
description: WebSocket upgrade
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/pty:
|
/v1/capsules/{id}/pty:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -1217,7 +1217,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: Interactive PTY session via WebSocket
|
summary: Interactive PTY session via WebSocket
|
||||||
operationId: ptySession
|
operationId: ptySession
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
description: |
|
description: |
|
||||||
@ -1266,25 +1266,25 @@ paths:
|
|||||||
|
|
||||||
Sessions have a 120-second inactivity timeout (reset on input/resize).
|
Sessions have a 120-second inactivity timeout (reset on input/resize).
|
||||||
Sessions persist across WebSocket disconnections — the process keeps
|
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.
|
reconnect later.
|
||||||
responses:
|
responses:
|
||||||
"101":
|
"101":
|
||||||
description: WebSocket upgrade
|
description: WebSocket upgrade
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/files/stream/write:
|
/v1/capsules/{id}/files/stream/write:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -1295,11 +1295,11 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: Upload a file (streaming)
|
summary: Upload a file (streaming)
|
||||||
operationId: streamUploadFile
|
operationId: streamUploadFile
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
description: |
|
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
|
Suitable for large files. Uses the same multipart/form-data format
|
||||||
as the non-streaming upload endpoint.
|
as the non-streaming upload endpoint.
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -1312,7 +1312,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
description: Absolute destination path inside the sandbox
|
description: Absolute destination path inside the capsule
|
||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
@ -1321,19 +1321,19 @@ paths:
|
|||||||
"204":
|
"204":
|
||||||
description: File uploaded
|
description: File uploaded
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox not found
|
description: Capsule not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
||||||
/v1/sandboxes/{id}/files/stream/read:
|
/v1/capsules/{id}/files/stream/read:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@ -1344,11 +1344,11 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: Download a file (streaming)
|
summary: Download a file (streaming)
|
||||||
operationId: streamDownloadFile
|
operationId: streamDownloadFile
|
||||||
tags: [sandboxes]
|
tags: [capsules]
|
||||||
security:
|
security:
|
||||||
- apiKeyAuth: []
|
- apiKeyAuth: []
|
||||||
description: |
|
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.
|
Suitable for large files. Returns raw bytes with chunked transfer encoding.
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@ -1365,13 +1365,13 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
"404":
|
"404":
|
||||||
description: Sandbox or file not found
|
description: Capsule or file not found
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Sandbox not running
|
description: Capsule not running
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@ -1469,14 +1469,14 @@ paths:
|
|||||||
description: |
|
description: |
|
||||||
Admins can delete any host. Team owners and admins can delete BYOC hosts
|
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
|
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:
|
parameters:
|
||||||
- name: force
|
- name: force
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
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:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
description: Host deleted
|
description: Host deleted
|
||||||
@ -1487,11 +1487,11 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
"409":
|
"409":
|
||||||
description: Host has active sandboxes (only when force is not set)
|
description: Host has active capsulees (only when force is not set)
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/HostHasSandboxesError"
|
$ref: "#/components/schemas/HostHasCapsulesError"
|
||||||
|
|
||||||
/v1/hosts/{id}/token:
|
/v1/hosts/{id}/token:
|
||||||
parameters:
|
parameters:
|
||||||
@ -1644,7 +1644,7 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
description: |
|
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.
|
were deleted with `?force=true`. No state is modified.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
@ -1917,7 +1917,7 @@ components:
|
|||||||
type: apiKey
|
type: apiKey
|
||||||
in: header
|
in: header
|
||||||
name: X-API-Key
|
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:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@ -2002,7 +2002,7 @@ components:
|
|||||||
description: Full plaintext key. Only returned on creation, never again.
|
description: Full plaintext key. Only returned on creation, never again.
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
||||||
CreateSandboxRequest:
|
CreateCapsuleRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
template:
|
template:
|
||||||
@ -2018,11 +2018,11 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
default: 0
|
default: 0
|
||||||
description: >
|
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
|
after this duration of inactivity (no exec or ping). 0 means
|
||||||
no auto-pause.
|
no auto-pause.
|
||||||
|
|
||||||
SandboxStats:
|
CapsuleStats:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
range:
|
range:
|
||||||
@ -2073,7 +2073,7 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
Sandbox:
|
Capsule:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
@ -2114,7 +2114,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
sandbox_id:
|
sandbox_id:
|
||||||
type: string
|
type: string
|
||||||
description: ID of the running sandbox to snapshot.
|
description: ID of the running capsule to snapshot.
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: Name for the snapshot template. Auto-generated if omitted.
|
description: Name for the snapshot template. Auto-generated if omitted.
|
||||||
@ -2180,7 +2180,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
description: Absolute file path inside the sandbox
|
description: Absolute file path inside the capsule
|
||||||
|
|
||||||
ListDirRequest:
|
ListDirRequest:
|
||||||
type: object
|
type: object
|
||||||
@ -2188,7 +2188,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
description: Directory path inside the sandbox
|
description: Directory path inside the capsule
|
||||||
depth:
|
depth:
|
||||||
type: integer
|
type: integer
|
||||||
default: 1
|
default: 1
|
||||||
@ -2238,7 +2238,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
description: Directory path to create inside the sandbox
|
description: Directory path to create inside the capsule
|
||||||
|
|
||||||
MakeDirResponse:
|
MakeDirResponse:
|
||||||
type: object
|
type: object
|
||||||
@ -2252,7 +2252,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
description: Path to remove inside the sandbox
|
description: Path to remove inside the capsule
|
||||||
|
|
||||||
CreateHostRequest:
|
CreateHostRequest:
|
||||||
type: object
|
type: object
|
||||||
@ -2390,9 +2390,9 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
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
|
type: object
|
||||||
properties:
|
properties:
|
||||||
error:
|
error:
|
||||||
@ -2407,7 +2407,7 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: IDs of active sandboxes blocking deletion.
|
description: IDs of active capsulees blocking deletion.
|
||||||
|
|
||||||
AddTagRequest:
|
AddTagRequest:
|
||||||
type: object
|
type: object
|
||||||
@ -2471,7 +2471,7 @@ components:
|
|||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/TeamMember"
|
$ref: "#/components/schemas/TeamMember"
|
||||||
|
|
||||||
SandboxMetrics:
|
CapsuleMetrics:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sandbox_id:
|
sandbox_id:
|
||||||
|
|||||||
@ -116,8 +116,8 @@ func New(
|
|||||||
// JWT-authenticated: user search (for add-member UI).
|
// JWT-authenticated: user search (for add-member UI).
|
||||||
r.With(requireJWT(jwtSecret)).Get("/v1/users/search", usersH.Search)
|
r.With(requireJWT(jwtSecret)).Get("/v1/users/search", usersH.Search)
|
||||||
|
|
||||||
// Sandbox lifecycle: accepts API key or JWT bearer token.
|
// Capsule lifecycle: accepts API key or JWT bearer token.
|
||||||
r.Route("/v1/sandboxes", func(r chi.Router) {
|
r.Route("/v1/capsules", func(r chi.Router) {
|
||||||
r.Use(requireAPIKeyOrJWT(queries, jwtSecret))
|
r.Use(requireAPIKeyOrJWT(queries, jwtSecret))
|
||||||
r.Post("/", sandbox.Create)
|
r.Post("/", sandbox.Create)
|
||||||
r.Get("/", sandbox.List)
|
r.Get("/", sandbox.List)
|
||||||
@ -229,7 +229,7 @@ func serveDocs(w http.ResponseWriter, r *http.Request) {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui.css" integrity="sha384-rcbEi6xgdPk0iWkAQzT2F3FeBJXdG+ydrawGlfHAFIZG7wU6aKbQaRewysYpmrlW" crossorigin="anonymous">
|
||||||
<style>
|
<style>
|
||||||
body { margin: 0; background: #fafafa; }
|
body { margin: 0; background: #fafafa; }
|
||||||
|
|||||||
Reference in New Issue
Block a user