1
0
forked from wrenn/wrenn

Merge pull request 'Added browser based filesystem interactions' (#16) from feat/file-interactions into dev

Reviewed-on: wrenn/wrenn#16
This commit is contained in:
2026-04-10 13:40:39 +00:00
18 changed files with 2229 additions and 126 deletions

View File

@ -0,0 +1,124 @@
import { auth } from '$lib/auth.svelte';
import { type ApiResult } from '$lib/api/client';
export type FileEntry = {
name: string;
path: string;
type: 'file' | 'directory' | 'symlink';
size: number;
mode: number;
permissions: string;
owner: string;
group: string;
modified_at: number;
symlink_target?: string | null;
};
export type ListDirResponse = {
entries: FileEntry[];
};
const MAX_READABLE_SIZE = 10 * 1024 * 1024; // 10 MB
/**
* Whether a file can be previewed as text in the browser.
* Binary/unreadable extensions and files > 10 MB should be downloaded instead.
*/
const BINARY_EXTENSIONS = new Set([
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.avif', '.svg',
'.mp3', '.mp4', '.wav', '.ogg', '.flac', '.avi', '.mkv', '.mov', '.webm',
'.zip', '.tar', '.gz', '.bz2', '.xz', '.7z', '.rar', '.zst',
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a', '.class', '.pyc',
'.woff', '.woff2', '.ttf', '.otf', '.eot',
'.db', '.sqlite', '.sqlite3',
'.iso', '.img', '.dmg',
]);
export function isBinaryFile(name: string): boolean {
const dot = name.lastIndexOf('.');
if (dot === -1) return false;
return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
}
export function isFileTooLarge(size: number): boolean {
return size > MAX_READABLE_SIZE;
}
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const val = bytes / Math.pow(1024, 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>> {
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`, {
method: 'POST',
headers,
body: JSON.stringify({ path, depth }),
});
const data = await res.json();
if (!res.ok) return { ok: false, error: data?.error?.message ?? 'Failed to list directory' };
return { ok: true, data: data as ListDirResponse };
} catch {
return { ok: false, error: 'Unable to connect to the server' };
}
}
export async function readFile(sandboxId: 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`, {
method: 'POST',
headers,
body: JSON.stringify({ path }),
});
if (!res.ok) {
try {
const data = await res.json();
return { ok: false, error: data?.error?.message ?? 'Failed to read file' };
} catch {
return { ok: false, error: `HTTP ${res.status}` };
}
}
const blob = await res.blob();
const text = await blob.text();
return { ok: true, data: text };
} catch {
return { ok: false, error: 'Unable to connect to the server' };
}
}
export async function downloadFile(sandboxId: 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`, {
method: 'POST',
headers,
body: JSON.stringify({ path }),
});
if (!res.ok) throw new Error('Download failed');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}

View File

@ -0,0 +1,112 @@
<script lang="ts">
let { value }: { value: string } = $props();
let copied = $state(false);
let timer: ReturnType<typeof setTimeout> | null = null;
async function copy(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
try {
await navigator.clipboard.writeText(value);
copied = true;
if (timer) clearTimeout(timer);
timer = setTimeout(() => (copied = false), 1800);
} catch {
// Clipboard API unavailable
}
}
</script>
<button
onclick={copy}
class="copy-btn"
class:copied
aria-label="Copy to clipboard"
>
<span class="copy-btn-inner">
{#if copied}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="check-icon">
<polyline points="20 6 9 17 4 12" />
</svg>
{:else}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="clipboard-icon">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
{/if}
</span>
</button>
<style>
.copy-btn {
position: relative;
display: inline-flex;
align-items: center;
gap: 4px;
height: 22px;
padding: 0 4px;
border-radius: 4px;
color: var(--color-text-muted);
background: transparent;
border: 1px solid transparent;
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
flex-shrink: 0;
}
.copy-btn:hover {
color: var(--color-text-secondary);
background: var(--color-bg-4);
border-color: var(--color-border);
}
.copy-btn:active {
transform: scale(0.92);
}
/* ── Copied state ── */
.copy-btn.copied {
opacity: 1;
color: var(--color-accent-bright);
background: rgba(94, 140, 88, 0.1);
border-color: rgba(94, 140, 88, 0.25);
}
.copy-btn-inner {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
}
/* ── Clipboard icon — subtle nudge on hover ── */
.clipboard-icon {
transition: transform 0.15s ease;
}
.copy-btn:hover .clipboard-icon {
transform: translate(-0.5px, -0.5px);
}
/* ── Check icon draw animation ── */
.check-icon {
animation: checkDraw 0.3s cubic-bezier(0.25, 1, 0.5, 1) both;
}
.check-icon polyline {
stroke-dasharray: 24;
stroke-dashoffset: 24;
animation: drawCheck 0.3s cubic-bezier(0.25, 1, 0.5, 1) 0.05s forwards;
}
@keyframes drawCheck {
to { stroke-dashoffset: 0; }
}
@keyframes checkDraw {
0% { transform: scale(0.6); opacity: 0; }
50% { opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
</style>

View File

@ -0,0 +1,622 @@
<script lang="ts">
import {
listDir,
readFile,
downloadFile,
isBinaryFile,
isFileTooLarge,
formatFileSize,
type FileEntry,
} from '$lib/api/files';
type Props = {
sandboxId: string;
isRunning: boolean;
};
let { sandboxId, isRunning }: Props = $props();
// Directory navigation state
let currentPath = $state('~');
let entries = $state<FileEntry[]>([]);
let dirLoading = $state(false);
let dirError = $state<string | null>(null);
// File preview state
let selectedFile = $state<FileEntry | null>(null);
let fileContent = $state<string | null>(null);
let fileLoading = $state(false);
let fileError = $state<string | null>(null);
// Path input
let pathInput = $state('~');
let pathInputFocused = $state(false);
let pathInputEl = $state<HTMLInputElement | undefined>(undefined);
// Sorted entries: directories first, then files, alphabetical within each group
const sortedEntries = $derived(
[...entries].sort((a, b) => {
if (a.type === 'directory' && b.type !== 'directory') return -1;
if (a.type !== 'directory' && b.type === 'directory') return 1;
return a.name.localeCompare(b.name);
})
);
// Breadcrumb segments from currentPath
const breadcrumbs = $derived(() => {
const parts = currentPath.split('/').filter(Boolean);
const crumbs: { name: string; path: string }[] = [{ name: '/', path: '/' }];
for (let i = 0; i < parts.length; i++) {
crumbs.push({ name: parts[i], path: '/' + parts.slice(0, i + 1).join('/') });
}
return crumbs;
});
// Count of dirs vs files for the footer
const dirCount = $derived(entries.filter((e) => e.type === 'directory').length);
const fileCount = $derived(entries.filter((e) => e.type !== 'directory').length);
const canGoUp = $derived(currentPath !== '/' && currentPath.startsWith('/'));
async function navigateTo(path: string) {
currentPath = normalizePath(path);
pathInput = currentPath;
selectedFile = null;
fileContent = null;
fileError = null;
await loadDir();
}
function normalizePath(p: string): string {
// Let envd handle ~ expansion — pass through as-is
if (p === '~' || p.startsWith('~/')) {
return p;
}
if (!p.startsWith('/')) {
// Relative path — resolve against current directory
p = currentPath.replace(/\/$/, '') + '/' + p;
}
// Collapse .. and .
const parts = p.split('/').filter(Boolean);
const resolved: string[] = [];
for (const part of parts) {
if (part === '..') resolved.pop();
else if (part !== '.') resolved.push(part);
}
return '/' + resolved.join('/');
}
/** Derive the parent directory from an entry's absolute path. */
function parentFromEntry(entryPath: string): string {
const lastSlash = entryPath.lastIndexOf('/');
if (lastSlash <= 0) return '/';
return entryPath.slice(0, lastSlash);
}
async function loadDir() {
if (!isRunning) return;
dirLoading = true;
dirError = null;
const result = await listDir(sandboxId, currentPath);
if (result.ok) {
entries = result.data.entries ?? [];
// Resolve actual path when envd expanded ~ or a relative path
if (!currentPath.startsWith('/') && entries.length > 0) {
currentPath = parentFromEntry(entries[0].path);
pathInput = currentPath;
}
} else {
dirError = result.error;
entries = [];
}
dirLoading = false;
}
async function selectFile(entry: FileEntry) {
if (entry.type === 'directory') {
await navigateTo(entry.path);
return;
}
selectedFile = entry;
fileContent = null;
fileError = null;
// Check if we should preview or prompt download
if (isBinaryFile(entry.name) || isFileTooLarge(entry.size)) {
// Don't load content — the preview pane will show download prompt
return;
}
fileLoading = true;
const result = await readFile(sandboxId, entry.path);
if (result.ok) {
// Check if content appears to be binary (contains null bytes or mostly non-printable)
if (looksLikeBinary(result.data)) {
fileContent = null;
// Will show download prompt
} else {
fileContent = result.data;
}
} else {
fileError = result.error;
}
fileLoading = false;
}
function looksLikeBinary(text: string): boolean {
// Sample first 8KB for null bytes or high ratio of non-printable chars
const sample = text.slice(0, 8192);
let nonPrintable = 0;
for (let i = 0; i < sample.length; i++) {
const code = sample.charCodeAt(i);
if (code === 0) return true;
if (code < 32 && code !== 9 && code !== 10 && code !== 13) nonPrintable++;
}
return sample.length > 0 && nonPrintable / sample.length > 0.1;
}
async function handleDownload() {
if (!selectedFile) return;
try {
await downloadFile(sandboxId, selectedFile.path, selectedFile.name);
} catch {
fileError = 'Download failed';
}
}
function handlePathSubmit(e: SubmitEvent) {
e.preventDefault();
const target = pathInput.trim();
if (!target) return;
const resolved = normalizePath(target);
navigateOrOpenFile(resolved);
}
async function navigateOrOpenFile(path: string) {
// First try as directory
const dirResult = await listDir(sandboxId, path);
if (dirResult.ok) {
// Resolve actual path from entries (handles ~ expansion by envd)
const resolvedEntries = dirResult.data.entries ?? [];
let resolvedPath = path;
if (resolvedEntries.length > 0) {
// Derive parent dir from first entry's absolute path
const firstPath = resolvedEntries[0].path;
const lastSlash = firstPath.lastIndexOf('/');
if (lastSlash >= 0) {
resolvedPath = lastSlash === 0 ? '/' : firstPath.slice(0, lastSlash);
}
}
currentPath = resolvedPath;
pathInput = resolvedPath;
entries = resolvedEntries;
selectedFile = null;
fileContent = null;
fileError = null;
return;
}
// If directory listing failed, try reading as a file
// We need the parent dir to get the file entry info
const lastSlash = path.lastIndexOf('/');
const parentPath = lastSlash <= 0 ? '/' : path.slice(0, lastSlash);
const fileName = path.slice(lastSlash + 1);
// Navigate to parent directory
currentPath = parentPath;
pathInput = parentPath;
const parentResult = await listDir(sandboxId, parentPath);
if (parentResult.ok) {
entries = parentResult.data.entries ?? [];
// Find the file in parent listing
const found = entries.find((e) => e.name === fileName);
if (found && found.type !== 'directory') {
await selectFile(found);
} else {
dirError = `Not found: ${path}`;
}
} else {
dirError = parentResult.error;
entries = [];
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
(e.target as HTMLInputElement)?.blur();
}
}
function fileIcon(entry: FileEntry): string {
if (entry.type === 'directory') return 'dir';
if (entry.type === 'symlink') return 'link';
return 'file';
}
// File extension for subtle coloring
function fileExt(name: string): string {
const dot = name.lastIndexOf('.');
return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
}
// Load initial directory on mount, falling back to / if home can't be resolved
$effect(() => {
if (isRunning) {
loadDir().then(() => {
// If ~ couldn't be resolved (empty dir or error), fall back to /
if (!currentPath.startsWith('/')) {
currentPath = '/';
pathInput = '/';
if (dirError) loadDir();
}
});
}
});
</script>
<style>
.file-row {
transition: background-color 0.1s ease;
}
.file-row:hover {
background-color: var(--color-bg-3);
}
.file-row.active {
background-color: var(--color-accent-glow);
border-left: 2px solid var(--color-accent);
}
.file-row:not(.active) {
border-left: 2px solid transparent;
}
.preview-code {
tab-size: 4;
-moz-tab-size: 4;
}
/* Staggered row entrance */
@keyframes rowSlideIn {
from { opacity: 0; transform: translateX(-4px); }
to { opacity: 1; transform: translateX(0); }
}
.row-enter {
animation: rowSlideIn 0.15s ease both;
}
/* Line highlight on hover */
.code-line:hover .line-content {
background-color: var(--color-bg-3);
}
.code-line:hover .line-num {
color: var(--color-text-tertiary);
}
</style>
{#if !isRunning}
<div class="flex items-center gap-3 rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] px-5 py-4 m-8">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-muted)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
<span class="text-ui text-[var(--color-text-tertiary)]">
File browser requires a running capsule.
</span>
</div>
{:else}
<div class="flex flex-1 min-h-0">
<!-- Left panel: File tree -->
<div class="flex w-[380px] shrink-0 flex-col border-r border-[var(--color-border)] bg-[var(--color-bg-2)]">
<!-- Path input -->
<form onsubmit={handlePathSubmit} class="border-b border-[var(--color-border)] px-4 py-3">
<div class="flex items-center gap-2 rounded-[var(--radius-input)] border px-3 py-1.5 transition-colors duration-150
{pathInputFocused
? 'border-[var(--color-accent)]/50 bg-[var(--color-bg-0)]'
: 'border-[var(--color-border)] bg-[var(--color-bg-1)]'}">
<!-- Terminal prompt icon -->
<span class="shrink-0 font-mono text-badge text-[var(--color-text-muted)] select-none" aria-hidden="true">
$
</span>
<input
type="text"
bind:this={pathInputEl}
bind:value={pathInput}
onfocus={() => (pathInputFocused = true)}
onblur={() => (pathInputFocused = false)}
onkeydown={handleKeydown}
placeholder="Enter path..."
spellcheck="false"
autocomplete="off"
class="flex-1 bg-transparent font-mono text-meta text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-muted)]"
/>
<button
type="submit"
class="shrink-0 flex items-center gap-1 rounded-[var(--radius-button)] px-2 py-0.5 text-badge font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-accent-glow-mid)] hover:text-[var(--color-accent-mid)]"
>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12" />
<polyline points="12 5 19 12 12 19" />
</svg>
Go
</button>
</div>
</form>
<!-- Breadcrumbs -->
<div class="flex items-center gap-0.5 border-b border-[var(--color-border)] px-2 py-2 overflow-x-auto">
<!-- Up button -->
<button
onclick={() => navigateTo(currentPath + '/..')}
disabled={!canGoUp}
title="Go to parent directory"
class="shrink-0 flex items-center justify-center rounded-[3px] w-6 h-6 transition-colors
{canGoUp
? 'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-primary)]'
: 'text-[var(--color-text-muted)] opacity-30 cursor-not-allowed'}"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<span class="w-px h-4 bg-[var(--color-border)] shrink-0 mx-1"></span>
{#each breadcrumbs() as crumb, i}
{#if i > 0}
<svg class="shrink-0 text-[var(--color-text-muted)]" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6" />
</svg>
{/if}
<button
onclick={() => navigateTo(crumb.path)}
class="shrink-0 rounded-[3px] px-1.5 py-0.5 font-mono text-label transition-colors hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-primary)]
{i === breadcrumbs().length - 1
? 'text-[var(--color-text-primary)]'
: 'text-[var(--color-text-tertiary)]'}"
>
{#if i === 0}
<!-- Root icon -->
<svg class="inline -mt-px" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
{:else}
{crumb.name}
{/if}
</button>
{/each}
</div>
<!-- File list -->
<div class="flex-1 overflow-y-auto">
{#if dirLoading}
<div class="flex items-center justify-center py-12">
<div class="flex items-center gap-2 text-meta text-[var(--color-text-secondary)]">
<svg class="animate-spin" width="14" height="14" 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>
Loading...
</div>
</div>
{:else if dirError}
<div class="px-4 py-4">
<div class="flex items-start gap-2.5 rounded-[var(--radius-card)] border border-[var(--color-red)]/25 bg-[var(--color-red)]/6 px-3.5 py-3">
<svg class="mt-0.5 shrink-0 text-[var(--color-red)]" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span class="text-meta text-[var(--color-red)]">{dirError}</span>
</div>
</div>
{:else if entries.length === 0}
<div class="flex flex-col items-center justify-center py-16 gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-3)]" style="animation: iconFloat 3s ease-in-out infinite">
<svg class="text-[var(--color-text-muted)]" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
</div>
<span class="text-meta text-[var(--color-text-muted)]">Nothing here yet</span>
</div>
{:else}
{#each sortedEntries as entry, idx (entry.path)}
<button
onclick={() => selectFile(entry)}
class="file-row row-enter flex w-full items-center gap-3 px-4 py-[7px] text-left
{selectedFile?.path === entry.path ? 'active' : ''}"
style="animation-delay: {Math.min(idx * 12, 200)}ms"
>
<!-- Icon -->
{#if fileIcon(entry) === 'dir'}
<svg class="shrink-0 text-[var(--color-accent-mid)]" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
{:else if fileIcon(entry) === 'link'}
<svg class="shrink-0 text-[var(--color-blue)]" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
{:else}
<svg class="shrink-0 text-[var(--color-text-muted)]" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
{/if}
<!-- Name + metadata -->
<div class="flex flex-1 items-center gap-2 overflow-hidden">
<span class="truncate font-mono text-meta
{entry.type === 'directory'
? 'text-[var(--color-text-primary)]'
: 'text-[var(--color-text-secondary)]'}">
{entry.name}
</span>
{#if entry.type === 'symlink' && entry.symlink_target}
<span class="truncate font-mono text-badge text-[var(--color-text-muted)]">
&rarr; {entry.symlink_target}
</span>
{/if}
</div>
<!-- Size (files only) -->
{#if entry.type === 'file'}
<span class="shrink-0 font-mono text-badge text-[var(--color-text-muted)]">
{formatFileSize(entry.size)}
</span>
{/if}
<!-- Permissions -->
<span class="hidden shrink-0 font-mono text-badge text-[var(--color-text-muted)] xl:inline">
{entry.permissions}
</span>
</button>
{/each}
{/if}
</div>
<!-- Footer: entry count -->
{#if !dirLoading && !dirError && entries.length > 0}
<div class="border-t border-[var(--color-border)] px-4 py-2 flex items-center gap-3">
{#if dirCount > 0}
<span class="font-mono text-badge text-[var(--color-text-muted)]">
{dirCount} dir{dirCount !== 1 ? 's' : ''}
</span>
{/if}
{#if fileCount > 0}
<span class="font-mono text-badge text-[var(--color-text-muted)]">
{fileCount} file{fileCount !== 1 ? 's' : ''}
</span>
{/if}
</div>
{/if}
</div>
<!-- Right panel: File preview -->
<div class="flex flex-1 flex-col min-w-0 bg-[var(--color-bg-1)]">
{#if !selectedFile}
<!-- Empty state -->
<div class="flex flex-1 items-center justify-center">
<div class="flex flex-col items-center gap-3 text-center">
<div class="flex h-12 w-12 items-center justify-center rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)]" style="animation: iconFloat 3s ease-in-out infinite">
<svg class="text-[var(--color-text-muted)]" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
</div>
<div class="flex flex-col gap-1">
<span class="text-ui text-[var(--color-text-secondary)]">No file selected</span>
<span class="text-meta text-[var(--color-text-muted)]">Choose a file from the tree, or enter a path directly</span>
</div>
</div>
</div>
{:else}
<!-- File header -->
<div class="flex items-center justify-between border-b border-[var(--color-border)] bg-[var(--color-bg-2)] px-5 py-2.5">
<div class="flex items-center gap-2.5 overflow-hidden">
{#if isBinaryFile(selectedFile.name) || isFileTooLarge(selectedFile.size)}
<svg class="shrink-0 text-[var(--color-amber)]" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
{:else}
<svg class="shrink-0 text-[var(--color-accent-mid)]" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
{/if}
<span class="truncate font-mono text-meta text-[var(--color-text-primary)]">{selectedFile.path}</span>
{#if fileExt(selectedFile.name)}
<span class="shrink-0 rounded-[3px] bg-[var(--color-bg-4)] px-1.5 py-0.5 font-mono text-badge uppercase text-[var(--color-text-muted)]">
{fileExt(selectedFile.name)}
</span>
{/if}
</div>
<div class="flex items-center gap-3 shrink-0 ml-4">
<span class="font-mono text-badge text-[var(--color-text-muted)]">{formatFileSize(selectedFile.size)}</span>
<button
onclick={handleDownload}
class="flex items-center gap-1.5 rounded-[var(--radius-button)] border border-[var(--color-border)] bg-[var(--color-bg-3)] px-2.5 py-1 text-badge font-semibold uppercase tracking-[0.05em] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-primary)]"
>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
Download
</button>
</div>
</div>
<!-- File content -->
<div class="flex-1 overflow-auto">
{#if fileLoading}
<div class="flex items-center justify-center py-16">
<div class="flex items-center gap-2 text-meta text-[var(--color-text-secondary)]">
<svg class="animate-spin" width="14" height="14" 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>
Reading file...
</div>
</div>
{:else if fileError}
<div class="px-5 py-5">
<div class="flex items-start gap-2.5 rounded-[var(--radius-card)] border border-[var(--color-red)]/25 bg-[var(--color-red)]/6 px-3.5 py-3">
<svg class="mt-0.5 shrink-0 text-[var(--color-red)]" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span class="text-meta text-[var(--color-red)]">{fileError}</span>
</div>
</div>
{:else if isBinaryFile(selectedFile.name) || isFileTooLarge(selectedFile.size) || (selectedFile && fileContent === null && !fileLoading)}
<!-- Binary / too large / unreadable — download prompt -->
<div class="flex flex-1 items-center justify-center py-20">
<div class="flex flex-col items-center gap-5 text-center" style="animation: fadeUp 0.25s ease both">
<div class="flex h-14 w-14 items-center justify-center rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-3)]">
{#if isFileTooLarge(selectedFile.size)}
<svg class="text-[var(--color-amber)]" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
{:else}
<svg class="text-[var(--color-text-muted)]" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="9" y1="3" x2="9" y2="21" />
</svg>
{/if}
</div>
<div class="flex flex-col gap-1.5">
{#if isFileTooLarge(selectedFile.size)}
<span class="text-ui font-medium text-[var(--color-text-primary)]">Too large to preview</span>
<span class="text-meta text-[var(--color-text-tertiary)]">
{formatFileSize(selectedFile.size)} — preview limit is 10 MB
</span>
{:else}
<span class="text-ui font-medium text-[var(--color-text-primary)]">Binary file</span>
<span class="text-meta text-[var(--color-text-tertiary)]">
Cannot display as text — download to view
</span>
{/if}
</div>
<button
onclick={handleDownload}
class="mt-1 flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-accent)]/30 bg-[var(--color-accent-glow-mid)] px-4 py-2 text-meta font-semibold text-[var(--color-accent-bright)] transition-all duration-150 hover:border-[var(--color-accent)]/50 hover:bg-[var(--color-accent)]/15 hover:-translate-y-px active:translate-y-0"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
Download file
</button>
</div>
</div>
{:else if fileContent !== null}
<!-- Text preview with line numbers -->
<div style="animation: fadeUp 0.15s ease both">
<pre class="preview-code p-0 m-0"><code class="block">{#each fileContent.split('\n') as line, i}<div class="code-line flex"><span class="line-num sticky left-0 inline-block w-[52px] shrink-0 select-none border-r border-[var(--color-border)] bg-[var(--color-bg-1)] px-3 py-0 text-right font-mono text-badge leading-[1.65rem] text-[var(--color-text-muted)] transition-colors duration-75">{i + 1}</span><span class="line-content flex-1 whitespace-pre-wrap break-all px-4 py-0 font-mono text-meta leading-[1.65rem] text-[var(--color-text-secondary)] transition-colors duration-75">{line || ' '}</span></div>{/each}</code></pre>
</div>
{/if}
</div>
{/if}
</div>
</div>
{/if}

View File

@ -185,7 +185,7 @@
...BASE_CHART_OPTIONS.scales.y,
ticks: {
...BASE_CHART_OPTIONS.scales.y.ticks,
callback: (v: number) => `${v}`,
callback: (v: string | number) => `${v}`,
},
},
},
@ -215,7 +215,8 @@
tooltip: {
...BASE_CHART_OPTIONS.plugins.tooltip,
callbacks: {
label: (ctx: { parsed: { y: number } }) => ` ${ctx.parsed.y.toFixed(1)} GB`,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: (ctx: any) => ` ${ctx.parsed.y.toFixed(1)} GB`,
},
},
},
@ -225,7 +226,7 @@
...BASE_CHART_OPTIONS.scales.y,
ticks: {
...BASE_CHART_OPTIONS.scales.y.ticks,
callback: (v: number) => `${(+v).toFixed(1)} GB`,
callback: (v: string | number) => `${(+v).toFixed(1)} GB`,
},
},
},

View File

@ -1,5 +1,6 @@
<script lang="ts">
import AdminSidebar from '$lib/components/AdminSidebar.svelte';
import CopyButton from '$lib/components/CopyButton.svelte';
import { onMount, onDestroy } from 'svelte';
import { toast } from '$lib/toast.svelte';
import { formatDate, timeAgo } from '$lib/utils/format';
@ -262,7 +263,7 @@
</p>
</div>
<button
onclick={() => { showCreate = true; createError = null; createForm = { name: '', base_template: 'minimal', vcpus: 1, memory_mb: 512, recipe: '', healthcheck: '' }; }}
onclick={() => { showCreate = true; createError = null; createForm = { name: '', base_template: 'minimal', vcpus: 1, memory_mb: 512, recipe: '', healthcheck: '', skip_pre_post: false }; }}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-4 py-2 text-ui font-semibold text-white shadow-sm transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
@ -416,7 +417,10 @@
{#each templates as tmpl (tmpl.name)}
<tr class="border-b border-[var(--color-border)] last:border-0 transition-colors duration-200 hover:bg-[var(--color-bg-2)]">
<td class="px-4 py-3.5">
<span class="font-mono text-meta text-[var(--color-text-primary)]">{tmpl.name}</span>
<div class="flex items-center gap-1.5">
<span class="font-mono text-meta text-[var(--color-text-primary)]">{tmpl.name}</span>
<CopyButton value={tmpl.name} />
</div>
</td>
<td class="px-4 py-3.5">
{#if tmpl.type === 'snapshot'}

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import Sidebar from '$lib/components/Sidebar.svelte';
import CopyButton from '$lib/components/CopyButton.svelte';
import { capsuleRunningCount } from '$lib/capsule-store.svelte';
let { children } = $props();
@ -33,8 +34,11 @@
Capsules
</a>
<span class="text-[var(--color-text-muted)] select-none" style="font-size: 1.1rem"></span>
<span class="font-mono text-[1.1rem] leading-none text-[var(--color-text-bright)]">
{$page.params.id}
<span class="copy-host flex items-center gap-1.5">
<span class="font-mono text-[1.1rem] leading-none text-[var(--color-text-bright)]">
{$page.params.id}
</span>
<CopyButton value={$page.params.id} />
</span>
</div>
</div>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import CreateCapsuleDialog from '$lib/components/CreateCapsuleDialog.svelte';
import CopyButton from '$lib/components/CopyButton.svelte';
import { capsuleRunningCount } from '$lib/capsule-store.svelte';
import { onMount } from 'svelte';
import { toast } from '$lib/toast.svelte';
@ -457,6 +458,7 @@
{:else}
<a href="/dashboard/capsules/{capsule.id}" class="font-mono text-ui text-[var(--color-text-bright)] hover:text-[var(--color-accent-bright)] transition-colors duration-150">{capsule.id}</a>
{/if}
<CopyButton value={capsule.id} />
</div>
<!-- Template -->

View File

@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { getCapsule, type Capsule } from '$lib/api/capsules';
import FilesTab from '$lib/components/FilesTab.svelte';
import {
fetchSandboxMetrics,
METRIC_RANGES,
@ -31,6 +32,8 @@
let chartCpu: any = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let chartRam: any = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let ChartJS = $state<any>(null);
let pollInterval: ReturnType<typeof setInterval> | null = null;
const metricsAvailable = $derived(
@ -182,23 +185,13 @@
},
};
onMount(async () => {
const urlRange = new URLSearchParams(window.location.search).get('range');
if (urlRange && METRIC_RANGES.includes(urlRange as MetricRange)) {
range = urlRange as MetricRange;
}
function initCharts() {
if (!ChartJS || !canvasCpu || !canvasRam) return;
await loadCapsule();
chartCpu?.destroy();
chartRam?.destroy();
if (!metricsAvailable) return;
await tick();
if (!canvasCpu || !canvasRam) return;
const { Chart } = await import('chart.js/auto');
chartCpu = new Chart(canvasCpu, {
chartCpu = new ChartJS(canvasCpu, {
type: 'line',
data: {
labels: [],
@ -241,7 +234,7 @@
},
});
chartRam = new Chart(canvasRam, {
chartRam = new ChartJS(canvasRam, {
type: 'line',
data: {
labels: [],
@ -285,7 +278,43 @@
});
updateCharts();
restartPolling();
}
// Re-create charts whenever the metrics tab becomes active (canvases remount)
$effect(() => {
// Only track these two values for re-triggering
const tab = activeTab;
const chartLib = ChartJS;
if (tab !== 'metrics' || !chartLib) return;
// Wait for canvases to mount after the tab switch
tick().then(() => {
if (canvasCpu && canvasRam) {
initCharts();
restartPolling();
}
});
return () => {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
chartCpu?.destroy(); chartCpu = null;
chartRam?.destroy(); chartRam = null;
};
});
onMount(async () => {
const urlRange = new URLSearchParams(window.location.search).get('range');
if (urlRange && METRIC_RANGES.includes(urlRange as MetricRange)) {
range = urlRange as MetricRange;
}
await loadCapsule();
if (!metricsAvailable) return;
const mod = await import('chart.js/auto');
ChartJS = mod.Chart;
});
onDestroy(() => {
@ -391,22 +420,25 @@
</button>
<button
disabled
title="Coming soon"
class="flex cursor-not-allowed items-center gap-2 border-b-2 border-transparent px-4 py-2.5 text-ui font-medium opacity-40"
onclick={() => (activeTab = 'files')}
class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-ui font-medium transition-colors duration-150
{activeTab === 'files'
? 'border-[var(--color-accent)] text-[var(--color-accent-bright)]'
: 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
Files
<span class="rounded-[3px] bg-[var(--color-bg-4)] px-1.5 py-0.5 text-badge font-semibold uppercase tracking-[0.06em] text-[var(--color-text-muted)]">
Soon
</span>
</button>
</div>
<!-- Stats tab content -->
{#if activeTab === 'metrics'}
{#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'} />
</div>
{:else if activeTab === 'metrics'}
<div
class="anim-in flex flex-1 flex-col gap-5 min-h-0 p-8"
style="animation-delay: 0.05s"

View File

@ -1,5 +1,6 @@
<script lang="ts">
import Sidebar from '$lib/components/Sidebar.svelte';
import CopyButton from '$lib/components/CopyButton.svelte';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { fly } from 'svelte/transition';
@ -350,7 +351,10 @@
<!-- Name -->
<div class="min-w-0 px-5 py-4">
<span class="block truncate font-mono text-ui text-[var(--color-text-bright)]">{snapshot.name}</span>
<div class="flex items-center gap-1.5">
<span class="block truncate font-mono text-ui text-[var(--color-text-bright)]">{snapshot.name}</span>
<CopyButton value={snapshot.name} />
</div>
</div>
<!-- Type badge -->

236
internal/api/handlers_fs.go Normal file
View File

@ -0,0 +1,236 @@
package api
import (
"net/http"
"connectrpc.com/connect"
"github.com/go-chi/chi/v5"
"git.omukk.dev/wrenn/wrenn/internal/auth"
"git.omukk.dev/wrenn/wrenn/internal/db"
"git.omukk.dev/wrenn/wrenn/internal/id"
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
)
type fsHandler struct {
db *db.Queries
pool *lifecycle.HostClientPool
}
func newFSHandler(db *db.Queries, pool *lifecycle.HostClientPool) *fsHandler {
return &fsHandler{db: db, pool: pool}
}
type listDirRequest struct {
Path string `json:"path"`
Depth uint32 `json:"depth"`
}
type fileEntryResponse struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
Size int64 `json:"size"`
Mode uint32 `json:"mode"`
Permissions string `json:"permissions"`
Owner string `json:"owner"`
Group string `json:"group"`
ModifiedAt int64 `json:"modified_at"`
SymlinkTarget *string `json:"symlink_target,omitempty"`
}
type listDirResponse struct {
Entries []fileEntryResponse `json:"entries"`
}
type makeDirRequest struct {
Path string `json:"path"`
}
type makeDirResponse struct {
Entry fileEntryResponse `json:"entry"`
}
type removeRequest struct {
Path string `json:"path"`
}
// ListDir handles POST /v1/sandboxes/{id}/files/list.
func (h *fsHandler) ListDir(w http.ResponseWriter, r *http.Request) {
sandboxIDStr := chi.URLParam(r, "id")
ctx := r.Context()
ac := auth.MustFromContext(ctx)
sandboxID, err := id.ParseSandboxID(sandboxIDStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID")
return
}
sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID})
if err != nil {
writeError(w, http.StatusNotFound, "not_found", "sandbox not found")
return
}
if sb.Status != "running" {
writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running")
return
}
var req listDirRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body")
return
}
if req.Path == "" {
writeError(w, http.StatusBadRequest, "invalid_request", "path is required")
return
}
agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID)
if err != nil {
writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable")
return
}
resp, err := agent.ListDir(ctx, connect.NewRequest(&pb.ListDirRequest{
SandboxId: sandboxIDStr,
Path: req.Path,
Depth: req.Depth,
}))
if err != nil {
status, code, msg := agentErrToHTTP(err)
writeError(w, status, code, msg)
return
}
entries := make([]fileEntryResponse, 0, len(resp.Msg.Entries))
for _, e := range resp.Msg.Entries {
entries = append(entries, fileEntryFromPB(e))
}
writeJSON(w, http.StatusOK, listDirResponse{Entries: entries})
}
// MakeDir handles POST /v1/sandboxes/{id}/files/mkdir.
func (h *fsHandler) MakeDir(w http.ResponseWriter, r *http.Request) {
sandboxIDStr := chi.URLParam(r, "id")
ctx := r.Context()
ac := auth.MustFromContext(ctx)
sandboxID, err := id.ParseSandboxID(sandboxIDStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID")
return
}
sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID})
if err != nil {
writeError(w, http.StatusNotFound, "not_found", "sandbox not found")
return
}
if sb.Status != "running" {
writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running")
return
}
var req makeDirRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body")
return
}
if req.Path == "" {
writeError(w, http.StatusBadRequest, "invalid_request", "path is required")
return
}
agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID)
if err != nil {
writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable")
return
}
resp, err := agent.MakeDir(ctx, connect.NewRequest(&pb.MakeDirRequest{
SandboxId: sandboxIDStr,
Path: req.Path,
}))
if err != nil {
status, code, msg := agentErrToHTTP(err)
writeError(w, status, code, msg)
return
}
writeJSON(w, http.StatusOK, makeDirResponse{Entry: fileEntryFromPB(resp.Msg.Entry)})
}
// Remove handles POST /v1/sandboxes/{id}/files/remove.
func (h *fsHandler) Remove(w http.ResponseWriter, r *http.Request) {
sandboxIDStr := chi.URLParam(r, "id")
ctx := r.Context()
ac := auth.MustFromContext(ctx)
sandboxID, err := id.ParseSandboxID(sandboxIDStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID")
return
}
sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID})
if err != nil {
writeError(w, http.StatusNotFound, "not_found", "sandbox not found")
return
}
if sb.Status != "running" {
writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running")
return
}
var req removeRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body")
return
}
if req.Path == "" {
writeError(w, http.StatusBadRequest, "invalid_request", "path is required")
return
}
agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID)
if err != nil {
writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable")
return
}
if _, err := agent.RemovePath(ctx, connect.NewRequest(&pb.RemovePathRequest{
SandboxId: sandboxIDStr,
Path: req.Path,
})); err != nil {
status, code, msg := agentErrToHTTP(err)
writeError(w, status, code, msg)
return
}
w.WriteHeader(http.StatusNoContent)
}
func fileEntryFromPB(e *pb.FileEntry) fileEntryResponse {
if e == nil {
return fileEntryResponse{}
}
resp := fileEntryResponse{
Name: e.Name,
Path: e.Path,
Type: e.Type,
Size: e.Size,
Mode: e.Mode,
Permissions: e.Permissions,
Owner: e.Owner,
Group: e.Group,
ModifiedAt: e.ModifiedAt,
}
if e.SymlinkTarget != nil {
resp.SymlinkTarget = e.SymlinkTarget
}
return resp
}

View File

@ -1037,6 +1037,122 @@ paths:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/files/list:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: List directory contents
operationId: listDir
tags: [sandboxes]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ListDirRequest"
responses:
"200":
description: Directory listing
content:
application/json:
schema:
$ref: "#/components/schemas/ListDirResponse"
"404":
description: Sandbox not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/files/mkdir:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Create a directory
operationId: makeDir
tags: [sandboxes]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MakeDirRequest"
responses:
"200":
description: Directory created
content:
application/json:
schema:
$ref: "#/components/schemas/MakeDirResponse"
"404":
description: Sandbox not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/files/remove:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Remove a file or directory
operationId: removePath
tags: [sandboxes]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RemoveRequest"
responses:
"204":
description: File or directory removed
"404":
description: Sandbox not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/exec/stream:
parameters:
- name: id
@ -1988,6 +2104,78 @@ components:
type: string
description: Absolute file path inside the sandbox
ListDirRequest:
type: object
required: [path]
properties:
path:
type: string
description: Directory path inside the sandbox
depth:
type: integer
default: 1
description: Recursion depth (0 = non-recursive, 1 = immediate children)
ListDirResponse:
type: object
properties:
entries:
type: array
items:
$ref: "#/components/schemas/FileEntry"
FileEntry:
type: object
properties:
name:
type: string
path:
type: string
type:
type: string
enum: [file, directory, symlink]
size:
type: integer
format: int64
mode:
type: integer
permissions:
type: string
description: Human-readable permissions (e.g. "-rwxr-xr-x")
owner:
type: string
group:
type: string
modified_at:
type: integer
format: int64
description: Unix timestamp (seconds)
symlink_target:
type: string
nullable: true
MakeDirRequest:
type: object
required: [path]
properties:
path:
type: string
description: Directory path to create inside the sandbox
MakeDirResponse:
type: object
properties:
entry:
$ref: "#/components/schemas/FileEntry"
RemoveRequest:
type: object
required: [path]
properties:
path:
type: string
description: Path to remove inside the sandbox
CreateHostRequest:
type: object
required: [type]

View File

@ -60,6 +60,7 @@ func New(
execStream := newExecStreamHandler(queries, pool)
files := newFilesHandler(queries, pool)
filesStream := newFilesStreamHandler(queries, pool)
fsH := newFSHandler(queries, pool)
snapshots := newSnapshotHandler(templateSvc, queries, pool, al)
authH := newAuthHandler(queries, pgPool, jwtSecret)
oauthH := newOAuthHandler(queries, pgPool, jwtSecret, oauthRegistry, oauthRedirectURL)
@ -133,6 +134,9 @@ func New(
r.Post("/files/read", files.Download)
r.Post("/files/stream/write", filesStream.StreamUpload)
r.Post("/files/stream/read", filesStream.StreamDownload)
r.Post("/files/list", fsH.ListDir)
r.Post("/files/mkdir", fsH.MakeDir)
r.Post("/files/remove", fsH.Remove)
r.Get("/metrics", metricsH.GetMetrics)
})
})

View File

@ -268,6 +268,30 @@ func (c *Client) ReadFile(ctx context.Context, path string) ([]byte, error) {
return data, nil
}
// PostInit calls envd's POST /init endpoint, which triggers a re-read of
// Firecracker MMDS metadata. This updates WRENN_SANDBOX_ID, WRENN_TEMPLATE_ID
// env vars and the corresponding files under /run/wrenn/ inside the guest.
// Must be called after snapshot restore so envd picks up the new sandbox's metadata.
func (c *Client) PostInit(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.base+"/init", nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("post init: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("post init: status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// ListDir lists directory contents inside the sandbox.
func (c *Client) ListDir(ctx context.Context, path string, depth uint32) (*envdpb.ListDirResponse, error) {
req := connect.NewRequest(&envdpb.ListDirRequest{
@ -282,3 +306,30 @@ func (c *Client) ListDir(ctx context.Context, path string, depth uint32) (*envdp
return resp.Msg, nil
}
// MakeDir creates a directory inside the sandbox.
func (c *Client) MakeDir(ctx context.Context, path string) (*envdpb.MakeDirResponse, error) {
req := connect.NewRequest(&envdpb.MakeDirRequest{
Path: path,
})
resp, err := c.filesystem.MakeDir(ctx, req)
if err != nil {
return nil, fmt.Errorf("make dir: %w", err)
}
return resp.Msg, nil
}
// Remove removes a file or directory inside the sandbox.
func (c *Client) Remove(ctx context.Context, path string) error {
req := connect.NewRequest(&envdpb.RemoveRequest{
Path: path,
})
if _, err := c.filesystem.Remove(ctx, req); err != nil {
return fmt.Errorf("remove: %w", err)
}
return nil
}

View File

@ -15,6 +15,7 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
envdpb "git.omukk.dev/wrenn/wrenn/proto/envd/gen"
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
@ -252,6 +253,69 @@ func (s *Server) ReadFile(
return connect.NewResponse(&pb.ReadFileResponse{Content: content}), nil
}
func (s *Server) ListDir(
ctx context.Context,
req *connect.Request[pb.ListDirRequest],
) (*connect.Response[pb.ListDirResponse], error) {
msg := req.Msg
client, err := s.mgr.GetClient(msg.SandboxId)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, err)
}
resp, err := client.ListDir(ctx, msg.Path, msg.Depth)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("list dir: %w", err))
}
entries := make([]*pb.FileEntry, 0, len(resp.Entries))
for _, e := range resp.Entries {
entries = append(entries, entryInfoToPB(e))
}
return connect.NewResponse(&pb.ListDirResponse{Entries: entries}), nil
}
func (s *Server) MakeDir(
ctx context.Context,
req *connect.Request[pb.MakeDirRequest],
) (*connect.Response[pb.MakeDirResponse], error) {
msg := req.Msg
client, err := s.mgr.GetClient(msg.SandboxId)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, err)
}
resp, err := client.MakeDir(ctx, msg.Path)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("make dir: %w", err))
}
return connect.NewResponse(&pb.MakeDirResponse{
Entry: entryInfoToPB(resp.Entry),
}), nil
}
func (s *Server) RemovePath(
ctx context.Context,
req *connect.Request[pb.RemovePathRequest],
) (*connect.Response[pb.RemovePathResponse], error) {
msg := req.Msg
client, err := s.mgr.GetClient(msg.SandboxId)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, err)
}
if err := client.Remove(ctx, msg.Path); err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("remove: %w", err))
}
return connect.NewResponse(&pb.RemovePathResponse{}), nil
}
func (s *Server) ExecStream(
ctx context.Context,
req *connect.Request[pb.ExecStreamRequest],
@ -545,3 +609,43 @@ func metricPointsToPB(pts []sandbox.MetricPoint) []*pb.MetricPoint {
}
return out
}
// entryInfoToPB maps an envd EntryInfo to a hostagent FileEntry.
func entryInfoToPB(e *envdpb.EntryInfo) *pb.FileEntry {
if e == nil {
return nil
}
var fileType string
switch e.Type {
case envdpb.FileType_FILE_TYPE_FILE:
fileType = "file"
case envdpb.FileType_FILE_TYPE_DIRECTORY:
fileType = "directory"
case envdpb.FileType_FILE_TYPE_SYMLINK:
fileType = "symlink"
default:
fileType = "unknown"
}
entry := &pb.FileEntry{
Name: e.Name,
Path: e.Path,
Type: fileType,
Size: e.Size,
Mode: e.Mode,
Permissions: e.Permissions,
Owner: e.Owner,
Group: e.Group,
}
if e.ModifiedTime != nil {
entry.ModifiedAt = e.ModifiedTime.GetSeconds()
}
if e.SymlinkTarget != nil {
entry.SymlinkTarget = e.SymlinkTarget
}
return entry
}

View File

@ -697,6 +697,11 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
return nil, fmt.Errorf("wait for envd: %w", err)
}
// Trigger envd to re-read MMDS so it picks up the new sandbox/template IDs.
if err := client.PostInit(waitCtx); err != nil {
slog.Warn("post-init failed after resume, metadata files may be stale", "sandbox", sandboxID, "error", err)
}
now := time.Now()
sb := &sandboxState{
Sandbox: models.Sandbox{
@ -1098,6 +1103,11 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
return nil, fmt.Errorf("wait for envd: %w", err)
}
// Trigger envd to re-read MMDS so it picks up the new sandbox/template IDs.
if err := client.PostInit(waitCtx); err != nil {
slog.Warn("post-init failed after template restore, metadata files may be stale", "sandbox", sandboxID, "error", err)
}
now := time.Now()
sb := &sandboxState{
Sandbox: models.Sandbox{

View File

@ -1833,6 +1833,413 @@ func (x *ReadFileStreamResponse) GetChunk() []byte {
return nil
}
type ListDirRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
Depth uint32 `protobuf:"varint,3,opt,name=depth,proto3" json:"depth,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDirRequest) Reset() {
*x = ListDirRequest{}
mi := &file_hostagent_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDirRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDirRequest) ProtoMessage() {}
func (x *ListDirRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListDirRequest.ProtoReflect.Descriptor instead.
func (*ListDirRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{31}
}
func (x *ListDirRequest) GetSandboxId() string {
if x != nil {
return x.SandboxId
}
return ""
}
func (x *ListDirRequest) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
func (x *ListDirRequest) GetDepth() uint32 {
if x != nil {
return x.Depth
}
return 0
}
type ListDirResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entries []*FileEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDirResponse) Reset() {
*x = ListDirResponse{}
mi := &file_hostagent_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDirResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDirResponse) ProtoMessage() {}
func (x *ListDirResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListDirResponse.ProtoReflect.Descriptor instead.
func (*ListDirResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{32}
}
func (x *ListDirResponse) GetEntries() []*FileEntry {
if x != nil {
return x.Entries
}
return nil
}
type FileEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
// "file", "directory", or "symlink".
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"`
Mode uint32 `protobuf:"varint,5,opt,name=mode,proto3" json:"mode,omitempty"`
// Human-readable permissions string, e.g. "-rwxr-xr-x".
Permissions string `protobuf:"bytes,6,opt,name=permissions,proto3" json:"permissions,omitempty"`
Owner string `protobuf:"bytes,7,opt,name=owner,proto3" json:"owner,omitempty"`
Group string `protobuf:"bytes,8,opt,name=group,proto3" json:"group,omitempty"`
// Last modification time as Unix timestamp (seconds).
ModifiedAt int64 `protobuf:"varint,9,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"`
SymlinkTarget *string `protobuf:"bytes,10,opt,name=symlink_target,json=symlinkTarget,proto3,oneof" json:"symlink_target,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FileEntry) Reset() {
*x = FileEntry{}
mi := &file_hostagent_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FileEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FileEntry) ProtoMessage() {}
func (x *FileEntry) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FileEntry.ProtoReflect.Descriptor instead.
func (*FileEntry) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{33}
}
func (x *FileEntry) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *FileEntry) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
func (x *FileEntry) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *FileEntry) GetSize() int64 {
if x != nil {
return x.Size
}
return 0
}
func (x *FileEntry) GetMode() uint32 {
if x != nil {
return x.Mode
}
return 0
}
func (x *FileEntry) GetPermissions() string {
if x != nil {
return x.Permissions
}
return ""
}
func (x *FileEntry) GetOwner() string {
if x != nil {
return x.Owner
}
return ""
}
func (x *FileEntry) GetGroup() string {
if x != nil {
return x.Group
}
return ""
}
func (x *FileEntry) GetModifiedAt() int64 {
if x != nil {
return x.ModifiedAt
}
return 0
}
func (x *FileEntry) GetSymlinkTarget() string {
if x != nil && x.SymlinkTarget != nil {
return *x.SymlinkTarget
}
return ""
}
type MakeDirRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *MakeDirRequest) Reset() {
*x = MakeDirRequest{}
mi := &file_hostagent_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *MakeDirRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MakeDirRequest) ProtoMessage() {}
func (x *MakeDirRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MakeDirRequest.ProtoReflect.Descriptor instead.
func (*MakeDirRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{34}
}
func (x *MakeDirRequest) GetSandboxId() string {
if x != nil {
return x.SandboxId
}
return ""
}
func (x *MakeDirRequest) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
type MakeDirResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entry *FileEntry `protobuf:"bytes,1,opt,name=entry,proto3" json:"entry,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *MakeDirResponse) Reset() {
*x = MakeDirResponse{}
mi := &file_hostagent_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *MakeDirResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MakeDirResponse) ProtoMessage() {}
func (x *MakeDirResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MakeDirResponse.ProtoReflect.Descriptor instead.
func (*MakeDirResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{35}
}
func (x *MakeDirResponse) GetEntry() *FileEntry {
if x != nil {
return x.Entry
}
return nil
}
type RemovePathRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RemovePathRequest) Reset() {
*x = RemovePathRequest{}
mi := &file_hostagent_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RemovePathRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemovePathRequest) ProtoMessage() {}
func (x *RemovePathRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemovePathRequest.ProtoReflect.Descriptor instead.
func (*RemovePathRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{36}
}
func (x *RemovePathRequest) GetSandboxId() string {
if x != nil {
return x.SandboxId
}
return ""
}
func (x *RemovePathRequest) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
type RemovePathResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RemovePathResponse) Reset() {
*x = RemovePathResponse{}
mi := &file_hostagent_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RemovePathResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemovePathResponse) ProtoMessage() {}
func (x *RemovePathResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemovePathResponse.ProtoReflect.Descriptor instead.
func (*RemovePathResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{37}
}
type PingSandboxRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
@ -1842,7 +2249,7 @@ type PingSandboxRequest struct {
func (x *PingSandboxRequest) Reset() {
*x = PingSandboxRequest{}
mi := &file_hostagent_proto_msgTypes[31]
mi := &file_hostagent_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1854,7 +2261,7 @@ func (x *PingSandboxRequest) String() string {
func (*PingSandboxRequest) ProtoMessage() {}
func (x *PingSandboxRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[31]
mi := &file_hostagent_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1867,7 +2274,7 @@ func (x *PingSandboxRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PingSandboxRequest.ProtoReflect.Descriptor instead.
func (*PingSandboxRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{31}
return file_hostagent_proto_rawDescGZIP(), []int{38}
}
func (x *PingSandboxRequest) GetSandboxId() string {
@ -1885,7 +2292,7 @@ type PingSandboxResponse struct {
func (x *PingSandboxResponse) Reset() {
*x = PingSandboxResponse{}
mi := &file_hostagent_proto_msgTypes[32]
mi := &file_hostagent_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1897,7 +2304,7 @@ func (x *PingSandboxResponse) String() string {
func (*PingSandboxResponse) ProtoMessage() {}
func (x *PingSandboxResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[32]
mi := &file_hostagent_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1910,7 +2317,7 @@ func (x *PingSandboxResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PingSandboxResponse.ProtoReflect.Descriptor instead.
func (*PingSandboxResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{32}
return file_hostagent_proto_rawDescGZIP(), []int{39}
}
type TerminateRequest struct {
@ -1921,7 +2328,7 @@ type TerminateRequest struct {
func (x *TerminateRequest) Reset() {
*x = TerminateRequest{}
mi := &file_hostagent_proto_msgTypes[33]
mi := &file_hostagent_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1933,7 +2340,7 @@ func (x *TerminateRequest) String() string {
func (*TerminateRequest) ProtoMessage() {}
func (x *TerminateRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[33]
mi := &file_hostagent_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1946,7 +2353,7 @@ func (x *TerminateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminateRequest.ProtoReflect.Descriptor instead.
func (*TerminateRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{33}
return file_hostagent_proto_rawDescGZIP(), []int{40}
}
type TerminateResponse struct {
@ -1957,7 +2364,7 @@ type TerminateResponse struct {
func (x *TerminateResponse) Reset() {
*x = TerminateResponse{}
mi := &file_hostagent_proto_msgTypes[34]
mi := &file_hostagent_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1969,7 +2376,7 @@ func (x *TerminateResponse) String() string {
func (*TerminateResponse) ProtoMessage() {}
func (x *TerminateResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[34]
mi := &file_hostagent_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1982,7 +2389,7 @@ func (x *TerminateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminateResponse.ProtoReflect.Descriptor instead.
func (*TerminateResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{34}
return file_hostagent_proto_rawDescGZIP(), []int{41}
}
type MetricPoint struct {
@ -1997,7 +2404,7 @@ type MetricPoint struct {
func (x *MetricPoint) Reset() {
*x = MetricPoint{}
mi := &file_hostagent_proto_msgTypes[35]
mi := &file_hostagent_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2009,7 +2416,7 @@ func (x *MetricPoint) String() string {
func (*MetricPoint) ProtoMessage() {}
func (x *MetricPoint) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[35]
mi := &file_hostagent_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2022,7 +2429,7 @@ func (x *MetricPoint) ProtoReflect() protoreflect.Message {
// Deprecated: Use MetricPoint.ProtoReflect.Descriptor instead.
func (*MetricPoint) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{35}
return file_hostagent_proto_rawDescGZIP(), []int{42}
}
func (x *MetricPoint) GetTimestampUnix() int64 {
@ -2064,7 +2471,7 @@ type GetSandboxMetricsRequest struct {
func (x *GetSandboxMetricsRequest) Reset() {
*x = GetSandboxMetricsRequest{}
mi := &file_hostagent_proto_msgTypes[36]
mi := &file_hostagent_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2076,7 +2483,7 @@ func (x *GetSandboxMetricsRequest) String() string {
func (*GetSandboxMetricsRequest) ProtoMessage() {}
func (x *GetSandboxMetricsRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[36]
mi := &file_hostagent_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2089,7 +2496,7 @@ func (x *GetSandboxMetricsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetSandboxMetricsRequest.ProtoReflect.Descriptor instead.
func (*GetSandboxMetricsRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{36}
return file_hostagent_proto_rawDescGZIP(), []int{43}
}
func (x *GetSandboxMetricsRequest) GetSandboxId() string {
@ -2115,7 +2522,7 @@ type GetSandboxMetricsResponse struct {
func (x *GetSandboxMetricsResponse) Reset() {
*x = GetSandboxMetricsResponse{}
mi := &file_hostagent_proto_msgTypes[37]
mi := &file_hostagent_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2127,7 +2534,7 @@ func (x *GetSandboxMetricsResponse) String() string {
func (*GetSandboxMetricsResponse) ProtoMessage() {}
func (x *GetSandboxMetricsResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[37]
mi := &file_hostagent_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2140,7 +2547,7 @@ func (x *GetSandboxMetricsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetSandboxMetricsResponse.ProtoReflect.Descriptor instead.
func (*GetSandboxMetricsResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{37}
return file_hostagent_proto_rawDescGZIP(), []int{44}
}
func (x *GetSandboxMetricsResponse) GetPoints() []*MetricPoint {
@ -2159,7 +2566,7 @@ type FlushSandboxMetricsRequest struct {
func (x *FlushSandboxMetricsRequest) Reset() {
*x = FlushSandboxMetricsRequest{}
mi := &file_hostagent_proto_msgTypes[38]
mi := &file_hostagent_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2171,7 +2578,7 @@ func (x *FlushSandboxMetricsRequest) String() string {
func (*FlushSandboxMetricsRequest) ProtoMessage() {}
func (x *FlushSandboxMetricsRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[38]
mi := &file_hostagent_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2184,7 +2591,7 @@ func (x *FlushSandboxMetricsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FlushSandboxMetricsRequest.ProtoReflect.Descriptor instead.
func (*FlushSandboxMetricsRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{38}
return file_hostagent_proto_rawDescGZIP(), []int{45}
}
func (x *FlushSandboxMetricsRequest) GetSandboxId() string {
@ -2205,7 +2612,7 @@ type FlushSandboxMetricsResponse struct {
func (x *FlushSandboxMetricsResponse) Reset() {
*x = FlushSandboxMetricsResponse{}
mi := &file_hostagent_proto_msgTypes[39]
mi := &file_hostagent_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2217,7 +2624,7 @@ func (x *FlushSandboxMetricsResponse) String() string {
func (*FlushSandboxMetricsResponse) ProtoMessage() {}
func (x *FlushSandboxMetricsResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[39]
mi := &file_hostagent_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2230,7 +2637,7 @@ func (x *FlushSandboxMetricsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FlushSandboxMetricsResponse.ProtoReflect.Descriptor instead.
func (*FlushSandboxMetricsResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{39}
return file_hostagent_proto_rawDescGZIP(), []int{46}
}
func (x *FlushSandboxMetricsResponse) GetPoints_10M() []*MetricPoint {
@ -2269,7 +2676,7 @@ type FlattenRootfsRequest struct {
func (x *FlattenRootfsRequest) Reset() {
*x = FlattenRootfsRequest{}
mi := &file_hostagent_proto_msgTypes[40]
mi := &file_hostagent_proto_msgTypes[47]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2281,7 +2688,7 @@ func (x *FlattenRootfsRequest) String() string {
func (*FlattenRootfsRequest) ProtoMessage() {}
func (x *FlattenRootfsRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[40]
mi := &file_hostagent_proto_msgTypes[47]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2294,7 +2701,7 @@ func (x *FlattenRootfsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FlattenRootfsRequest.ProtoReflect.Descriptor instead.
func (*FlattenRootfsRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{40}
return file_hostagent_proto_rawDescGZIP(), []int{47}
}
func (x *FlattenRootfsRequest) GetSandboxId() string {
@ -2334,7 +2741,7 @@ type FlattenRootfsResponse struct {
func (x *FlattenRootfsResponse) Reset() {
*x = FlattenRootfsResponse{}
mi := &file_hostagent_proto_msgTypes[41]
mi := &file_hostagent_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2346,7 +2753,7 @@ func (x *FlattenRootfsResponse) String() string {
func (*FlattenRootfsResponse) ProtoMessage() {}
func (x *FlattenRootfsResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[41]
mi := &file_hostagent_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2359,7 +2766,7 @@ func (x *FlattenRootfsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FlattenRootfsResponse.ProtoReflect.Descriptor instead.
func (*FlattenRootfsResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{41}
return file_hostagent_proto_rawDescGZIP(), []int{48}
}
func (x *FlattenRootfsResponse) GetSizeBytes() int64 {
@ -2505,7 +2912,39 @@ const file_hostagent_proto_rawDesc = "" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\".\n" +
"\x16ReadFileStreamResponse\x12\x14\n" +
"\x05chunk\x18\x01 \x01(\fR\x05chunk\"3\n" +
"\x05chunk\x18\x01 \x01(\fR\x05chunk\"Y\n" +
"\x0eListDirRequest\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\x12\x14\n" +
"\x05depth\x18\x03 \x01(\rR\x05depth\"D\n" +
"\x0fListDirResponse\x121\n" +
"\aentries\x18\x01 \x03(\v2\x17.hostagent.v1.FileEntryR\aentries\"\x9d\x02\n" +
"\tFileEntry\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" +
"\x04type\x18\x03 \x01(\tR\x04type\x12\x12\n" +
"\x04size\x18\x04 \x01(\x03R\x04size\x12\x12\n" +
"\x04mode\x18\x05 \x01(\rR\x04mode\x12 \n" +
"\vpermissions\x18\x06 \x01(\tR\vpermissions\x12\x14\n" +
"\x05owner\x18\a \x01(\tR\x05owner\x12\x14\n" +
"\x05group\x18\b \x01(\tR\x05group\x12\x1f\n" +
"\vmodified_at\x18\t \x01(\x03R\n" +
"modifiedAt\x12*\n" +
"\x0esymlink_target\x18\n" +
" \x01(\tH\x00R\rsymlinkTarget\x88\x01\x01B\x11\n" +
"\x0f_symlink_target\"C\n" +
"\x0eMakeDirRequest\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\"@\n" +
"\x0fMakeDirResponse\x12-\n" +
"\x05entry\x18\x01 \x01(\v2\x17.hostagent.v1.FileEntryR\x05entry\"F\n" +
"\x11RemovePathRequest\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\"\x14\n" +
"\x12RemovePathResponse\"3\n" +
"\x12PingSandboxRequest\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x15\n" +
@ -2542,7 +2981,7 @@ const file_hostagent_proto_rawDesc = "" +
"templateId\"6\n" +
"\x15FlattenRootfsResponse\x12\x1d\n" +
"\n" +
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes2\xc8\f\n" +
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes2\xa9\x0e\n" +
"\x10HostAgentService\x12X\n" +
"\rCreateSandbox\x12\".hostagent.v1.CreateSandboxRequest\x1a#.hostagent.v1.CreateSandboxResponse\x12[\n" +
"\x0eDestroySandbox\x12#.hostagent.v1.DestroySandboxRequest\x1a$.hostagent.v1.DestroySandboxResponse\x12U\n" +
@ -2551,7 +2990,11 @@ const file_hostagent_proto_rawDesc = "" +
"\x04Exec\x12\x19.hostagent.v1.ExecRequest\x1a\x1a.hostagent.v1.ExecResponse\x12X\n" +
"\rListSandboxes\x12\".hostagent.v1.ListSandboxesRequest\x1a#.hostagent.v1.ListSandboxesResponse\x12L\n" +
"\tWriteFile\x12\x1e.hostagent.v1.WriteFileRequest\x1a\x1f.hostagent.v1.WriteFileResponse\x12I\n" +
"\bReadFile\x12\x1d.hostagent.v1.ReadFileRequest\x1a\x1e.hostagent.v1.ReadFileResponse\x12[\n" +
"\bReadFile\x12\x1d.hostagent.v1.ReadFileRequest\x1a\x1e.hostagent.v1.ReadFileResponse\x12F\n" +
"\aListDir\x12\x1c.hostagent.v1.ListDirRequest\x1a\x1d.hostagent.v1.ListDirResponse\x12F\n" +
"\aMakeDir\x12\x1c.hostagent.v1.MakeDirRequest\x1a\x1d.hostagent.v1.MakeDirResponse\x12O\n" +
"\n" +
"RemovePath\x12\x1f.hostagent.v1.RemovePathRequest\x1a .hostagent.v1.RemovePathResponse\x12[\n" +
"\x0eCreateSnapshot\x12#.hostagent.v1.CreateSnapshotRequest\x1a$.hostagent.v1.CreateSnapshotResponse\x12[\n" +
"\x0eDeleteSnapshot\x12#.hostagent.v1.DeleteSnapshotRequest\x1a$.hostagent.v1.DeleteSnapshotResponse\x12Q\n" +
"\n" +
@ -2577,7 +3020,7 @@ func file_hostagent_proto_rawDescGZIP() []byte {
return file_hostagent_proto_rawDescData
}
var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 42)
var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 49)
var file_hostagent_proto_goTypes = []any{
(*CreateSandboxRequest)(nil), // 0: hostagent.v1.CreateSandboxRequest
(*CreateSandboxResponse)(nil), // 1: hostagent.v1.CreateSandboxResponse
@ -2610,17 +3053,24 @@ var file_hostagent_proto_goTypes = []any{
(*WriteFileStreamResponse)(nil), // 28: hostagent.v1.WriteFileStreamResponse
(*ReadFileStreamRequest)(nil), // 29: hostagent.v1.ReadFileStreamRequest
(*ReadFileStreamResponse)(nil), // 30: hostagent.v1.ReadFileStreamResponse
(*PingSandboxRequest)(nil), // 31: hostagent.v1.PingSandboxRequest
(*PingSandboxResponse)(nil), // 32: hostagent.v1.PingSandboxResponse
(*TerminateRequest)(nil), // 33: hostagent.v1.TerminateRequest
(*TerminateResponse)(nil), // 34: hostagent.v1.TerminateResponse
(*MetricPoint)(nil), // 35: hostagent.v1.MetricPoint
(*GetSandboxMetricsRequest)(nil), // 36: hostagent.v1.GetSandboxMetricsRequest
(*GetSandboxMetricsResponse)(nil), // 37: hostagent.v1.GetSandboxMetricsResponse
(*FlushSandboxMetricsRequest)(nil), // 38: hostagent.v1.FlushSandboxMetricsRequest
(*FlushSandboxMetricsResponse)(nil), // 39: hostagent.v1.FlushSandboxMetricsResponse
(*FlattenRootfsRequest)(nil), // 40: hostagent.v1.FlattenRootfsRequest
(*FlattenRootfsResponse)(nil), // 41: hostagent.v1.FlattenRootfsResponse
(*ListDirRequest)(nil), // 31: hostagent.v1.ListDirRequest
(*ListDirResponse)(nil), // 32: hostagent.v1.ListDirResponse
(*FileEntry)(nil), // 33: hostagent.v1.FileEntry
(*MakeDirRequest)(nil), // 34: hostagent.v1.MakeDirRequest
(*MakeDirResponse)(nil), // 35: hostagent.v1.MakeDirResponse
(*RemovePathRequest)(nil), // 36: hostagent.v1.RemovePathRequest
(*RemovePathResponse)(nil), // 37: hostagent.v1.RemovePathResponse
(*PingSandboxRequest)(nil), // 38: hostagent.v1.PingSandboxRequest
(*PingSandboxResponse)(nil), // 39: hostagent.v1.PingSandboxResponse
(*TerminateRequest)(nil), // 40: hostagent.v1.TerminateRequest
(*TerminateResponse)(nil), // 41: hostagent.v1.TerminateResponse
(*MetricPoint)(nil), // 42: hostagent.v1.MetricPoint
(*GetSandboxMetricsRequest)(nil), // 43: hostagent.v1.GetSandboxMetricsRequest
(*GetSandboxMetricsResponse)(nil), // 44: hostagent.v1.GetSandboxMetricsResponse
(*FlushSandboxMetricsRequest)(nil), // 45: hostagent.v1.FlushSandboxMetricsRequest
(*FlushSandboxMetricsResponse)(nil), // 46: hostagent.v1.FlushSandboxMetricsResponse
(*FlattenRootfsRequest)(nil), // 47: hostagent.v1.FlattenRootfsRequest
(*FlattenRootfsResponse)(nil), // 48: hostagent.v1.FlattenRootfsResponse
}
var file_hostagent_proto_depIdxs = []int32{
16, // 0: hostagent.v1.ListSandboxesResponse.sandboxes:type_name -> hostagent.v1.SandboxInfo
@ -2628,51 +3078,59 @@ var file_hostagent_proto_depIdxs = []int32{
24, // 2: hostagent.v1.ExecStreamResponse.data:type_name -> hostagent.v1.ExecStreamData
25, // 3: hostagent.v1.ExecStreamResponse.end:type_name -> hostagent.v1.ExecStreamEnd
27, // 4: hostagent.v1.WriteFileStreamRequest.meta:type_name -> hostagent.v1.WriteFileStreamMeta
35, // 5: hostagent.v1.GetSandboxMetricsResponse.points:type_name -> hostagent.v1.MetricPoint
35, // 6: hostagent.v1.FlushSandboxMetricsResponse.points_10m:type_name -> hostagent.v1.MetricPoint
35, // 7: hostagent.v1.FlushSandboxMetricsResponse.points_2h:type_name -> hostagent.v1.MetricPoint
35, // 8: hostagent.v1.FlushSandboxMetricsResponse.points_24h:type_name -> hostagent.v1.MetricPoint
0, // 9: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest
2, // 10: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest
4, // 11: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest
6, // 12: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest
12, // 13: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest
14, // 14: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest
17, // 15: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest
19, // 16: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest
8, // 17: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest
10, // 18: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest
21, // 19: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest
26, // 20: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest
29, // 21: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest
31, // 22: hostagent.v1.HostAgentService.PingSandbox:input_type -> hostagent.v1.PingSandboxRequest
33, // 23: hostagent.v1.HostAgentService.Terminate:input_type -> hostagent.v1.TerminateRequest
36, // 24: hostagent.v1.HostAgentService.GetSandboxMetrics:input_type -> hostagent.v1.GetSandboxMetricsRequest
38, // 25: hostagent.v1.HostAgentService.FlushSandboxMetrics:input_type -> hostagent.v1.FlushSandboxMetricsRequest
40, // 26: hostagent.v1.HostAgentService.FlattenRootfs:input_type -> hostagent.v1.FlattenRootfsRequest
1, // 27: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse
3, // 28: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse
5, // 29: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse
7, // 30: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse
13, // 31: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse
15, // 32: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse
18, // 33: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse
20, // 34: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse
9, // 35: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse
11, // 36: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse
22, // 37: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse
28, // 38: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse
30, // 39: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse
32, // 40: hostagent.v1.HostAgentService.PingSandbox:output_type -> hostagent.v1.PingSandboxResponse
34, // 41: hostagent.v1.HostAgentService.Terminate:output_type -> hostagent.v1.TerminateResponse
37, // 42: hostagent.v1.HostAgentService.GetSandboxMetrics:output_type -> hostagent.v1.GetSandboxMetricsResponse
39, // 43: hostagent.v1.HostAgentService.FlushSandboxMetrics:output_type -> hostagent.v1.FlushSandboxMetricsResponse
41, // 44: hostagent.v1.HostAgentService.FlattenRootfs:output_type -> hostagent.v1.FlattenRootfsResponse
27, // [27:45] is the sub-list for method output_type
9, // [9:27] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
33, // 5: hostagent.v1.ListDirResponse.entries:type_name -> hostagent.v1.FileEntry
33, // 6: hostagent.v1.MakeDirResponse.entry:type_name -> hostagent.v1.FileEntry
42, // 7: hostagent.v1.GetSandboxMetricsResponse.points:type_name -> hostagent.v1.MetricPoint
42, // 8: hostagent.v1.FlushSandboxMetricsResponse.points_10m:type_name -> hostagent.v1.MetricPoint
42, // 9: hostagent.v1.FlushSandboxMetricsResponse.points_2h:type_name -> hostagent.v1.MetricPoint
42, // 10: hostagent.v1.FlushSandboxMetricsResponse.points_24h:type_name -> hostagent.v1.MetricPoint
0, // 11: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest
2, // 12: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest
4, // 13: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest
6, // 14: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest
12, // 15: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest
14, // 16: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest
17, // 17: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest
19, // 18: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest
31, // 19: hostagent.v1.HostAgentService.ListDir:input_type -> hostagent.v1.ListDirRequest
34, // 20: hostagent.v1.HostAgentService.MakeDir:input_type -> hostagent.v1.MakeDirRequest
36, // 21: hostagent.v1.HostAgentService.RemovePath:input_type -> hostagent.v1.RemovePathRequest
8, // 22: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest
10, // 23: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest
21, // 24: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest
26, // 25: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest
29, // 26: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest
38, // 27: hostagent.v1.HostAgentService.PingSandbox:input_type -> hostagent.v1.PingSandboxRequest
40, // 28: hostagent.v1.HostAgentService.Terminate:input_type -> hostagent.v1.TerminateRequest
43, // 29: hostagent.v1.HostAgentService.GetSandboxMetrics:input_type -> hostagent.v1.GetSandboxMetricsRequest
45, // 30: hostagent.v1.HostAgentService.FlushSandboxMetrics:input_type -> hostagent.v1.FlushSandboxMetricsRequest
47, // 31: hostagent.v1.HostAgentService.FlattenRootfs:input_type -> hostagent.v1.FlattenRootfsRequest
1, // 32: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse
3, // 33: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse
5, // 34: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse
7, // 35: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse
13, // 36: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse
15, // 37: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse
18, // 38: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse
20, // 39: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse
32, // 40: hostagent.v1.HostAgentService.ListDir:output_type -> hostagent.v1.ListDirResponse
35, // 41: hostagent.v1.HostAgentService.MakeDir:output_type -> hostagent.v1.MakeDirResponse
37, // 42: hostagent.v1.HostAgentService.RemovePath:output_type -> hostagent.v1.RemovePathResponse
9, // 43: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse
11, // 44: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse
22, // 45: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse
28, // 46: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse
30, // 47: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse
39, // 48: hostagent.v1.HostAgentService.PingSandbox:output_type -> hostagent.v1.PingSandboxResponse
41, // 49: hostagent.v1.HostAgentService.Terminate:output_type -> hostagent.v1.TerminateResponse
44, // 50: hostagent.v1.HostAgentService.GetSandboxMetrics:output_type -> hostagent.v1.GetSandboxMetricsResponse
46, // 51: hostagent.v1.HostAgentService.FlushSandboxMetrics:output_type -> hostagent.v1.FlushSandboxMetricsResponse
48, // 52: hostagent.v1.HostAgentService.FlattenRootfs:output_type -> hostagent.v1.FlattenRootfsResponse
32, // [32:53] is the sub-list for method output_type
11, // [11:32] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_hostagent_proto_init() }
@ -2693,13 +3151,14 @@ func file_hostagent_proto_init() {
(*WriteFileStreamRequest_Meta)(nil),
(*WriteFileStreamRequest_Chunk)(nil),
}
file_hostagent_proto_msgTypes[33].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_hostagent_proto_rawDesc), len(file_hostagent_proto_rawDesc)),
NumEnums: 0,
NumMessages: 42,
NumMessages: 49,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -56,6 +56,15 @@ const (
// HostAgentServiceReadFileProcedure is the fully-qualified name of the HostAgentService's ReadFile
// RPC.
HostAgentServiceReadFileProcedure = "/hostagent.v1.HostAgentService/ReadFile"
// HostAgentServiceListDirProcedure is the fully-qualified name of the HostAgentService's ListDir
// RPC.
HostAgentServiceListDirProcedure = "/hostagent.v1.HostAgentService/ListDir"
// HostAgentServiceMakeDirProcedure is the fully-qualified name of the HostAgentService's MakeDir
// RPC.
HostAgentServiceMakeDirProcedure = "/hostagent.v1.HostAgentService/MakeDir"
// HostAgentServiceRemovePathProcedure is the fully-qualified name of the HostAgentService's
// RemovePath RPC.
HostAgentServiceRemovePathProcedure = "/hostagent.v1.HostAgentService/RemovePath"
// HostAgentServiceCreateSnapshotProcedure is the fully-qualified name of the HostAgentService's
// CreateSnapshot RPC.
HostAgentServiceCreateSnapshotProcedure = "/hostagent.v1.HostAgentService/CreateSnapshot"
@ -106,6 +115,12 @@ type HostAgentServiceClient interface {
WriteFile(context.Context, *connect.Request[gen.WriteFileRequest]) (*connect.Response[gen.WriteFileResponse], error)
// ReadFile reads a file from inside a sandbox.
ReadFile(context.Context, *connect.Request[gen.ReadFileRequest]) (*connect.Response[gen.ReadFileResponse], error)
// ListDir lists directory contents inside a sandbox.
ListDir(context.Context, *connect.Request[gen.ListDirRequest]) (*connect.Response[gen.ListDirResponse], error)
// MakeDir creates a directory inside a sandbox.
MakeDir(context.Context, *connect.Request[gen.MakeDirRequest]) (*connect.Response[gen.MakeDirResponse], error)
// RemovePath removes a file or directory inside a sandbox.
RemovePath(context.Context, *connect.Request[gen.RemovePathRequest]) (*connect.Response[gen.RemovePathResponse], error)
// CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable
// template, and destroys the sandbox.
CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error)
@ -195,6 +210,24 @@ func NewHostAgentServiceClient(httpClient connect.HTTPClient, baseURL string, op
connect.WithSchema(hostAgentServiceMethods.ByName("ReadFile")),
connect.WithClientOptions(opts...),
),
listDir: connect.NewClient[gen.ListDirRequest, gen.ListDirResponse](
httpClient,
baseURL+HostAgentServiceListDirProcedure,
connect.WithSchema(hostAgentServiceMethods.ByName("ListDir")),
connect.WithClientOptions(opts...),
),
makeDir: connect.NewClient[gen.MakeDirRequest, gen.MakeDirResponse](
httpClient,
baseURL+HostAgentServiceMakeDirProcedure,
connect.WithSchema(hostAgentServiceMethods.ByName("MakeDir")),
connect.WithClientOptions(opts...),
),
removePath: connect.NewClient[gen.RemovePathRequest, gen.RemovePathResponse](
httpClient,
baseURL+HostAgentServiceRemovePathProcedure,
connect.WithSchema(hostAgentServiceMethods.ByName("RemovePath")),
connect.WithClientOptions(opts...),
),
createSnapshot: connect.NewClient[gen.CreateSnapshotRequest, gen.CreateSnapshotResponse](
httpClient,
baseURL+HostAgentServiceCreateSnapshotProcedure,
@ -268,6 +301,9 @@ type hostAgentServiceClient struct {
listSandboxes *connect.Client[gen.ListSandboxesRequest, gen.ListSandboxesResponse]
writeFile *connect.Client[gen.WriteFileRequest, gen.WriteFileResponse]
readFile *connect.Client[gen.ReadFileRequest, gen.ReadFileResponse]
listDir *connect.Client[gen.ListDirRequest, gen.ListDirResponse]
makeDir *connect.Client[gen.MakeDirRequest, gen.MakeDirResponse]
removePath *connect.Client[gen.RemovePathRequest, gen.RemovePathResponse]
createSnapshot *connect.Client[gen.CreateSnapshotRequest, gen.CreateSnapshotResponse]
deleteSnapshot *connect.Client[gen.DeleteSnapshotRequest, gen.DeleteSnapshotResponse]
execStream *connect.Client[gen.ExecStreamRequest, gen.ExecStreamResponse]
@ -320,6 +356,21 @@ func (c *hostAgentServiceClient) ReadFile(ctx context.Context, req *connect.Requ
return c.readFile.CallUnary(ctx, req)
}
// ListDir calls hostagent.v1.HostAgentService.ListDir.
func (c *hostAgentServiceClient) ListDir(ctx context.Context, req *connect.Request[gen.ListDirRequest]) (*connect.Response[gen.ListDirResponse], error) {
return c.listDir.CallUnary(ctx, req)
}
// MakeDir calls hostagent.v1.HostAgentService.MakeDir.
func (c *hostAgentServiceClient) MakeDir(ctx context.Context, req *connect.Request[gen.MakeDirRequest]) (*connect.Response[gen.MakeDirResponse], error) {
return c.makeDir.CallUnary(ctx, req)
}
// RemovePath calls hostagent.v1.HostAgentService.RemovePath.
func (c *hostAgentServiceClient) RemovePath(ctx context.Context, req *connect.Request[gen.RemovePathRequest]) (*connect.Response[gen.RemovePathResponse], error) {
return c.removePath.CallUnary(ctx, req)
}
// CreateSnapshot calls hostagent.v1.HostAgentService.CreateSnapshot.
func (c *hostAgentServiceClient) CreateSnapshot(ctx context.Context, req *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error) {
return c.createSnapshot.CallUnary(ctx, req)
@ -388,6 +439,12 @@ type HostAgentServiceHandler interface {
WriteFile(context.Context, *connect.Request[gen.WriteFileRequest]) (*connect.Response[gen.WriteFileResponse], error)
// ReadFile reads a file from inside a sandbox.
ReadFile(context.Context, *connect.Request[gen.ReadFileRequest]) (*connect.Response[gen.ReadFileResponse], error)
// ListDir lists directory contents inside a sandbox.
ListDir(context.Context, *connect.Request[gen.ListDirRequest]) (*connect.Response[gen.ListDirResponse], error)
// MakeDir creates a directory inside a sandbox.
MakeDir(context.Context, *connect.Request[gen.MakeDirRequest]) (*connect.Response[gen.MakeDirResponse], error)
// RemovePath removes a file or directory inside a sandbox.
RemovePath(context.Context, *connect.Request[gen.RemovePathRequest]) (*connect.Response[gen.RemovePathResponse], error)
// CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable
// template, and destroys the sandbox.
CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error)
@ -473,6 +530,24 @@ func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.Han
connect.WithSchema(hostAgentServiceMethods.ByName("ReadFile")),
connect.WithHandlerOptions(opts...),
)
hostAgentServiceListDirHandler := connect.NewUnaryHandler(
HostAgentServiceListDirProcedure,
svc.ListDir,
connect.WithSchema(hostAgentServiceMethods.ByName("ListDir")),
connect.WithHandlerOptions(opts...),
)
hostAgentServiceMakeDirHandler := connect.NewUnaryHandler(
HostAgentServiceMakeDirProcedure,
svc.MakeDir,
connect.WithSchema(hostAgentServiceMethods.ByName("MakeDir")),
connect.WithHandlerOptions(opts...),
)
hostAgentServiceRemovePathHandler := connect.NewUnaryHandler(
HostAgentServiceRemovePathProcedure,
svc.RemovePath,
connect.WithSchema(hostAgentServiceMethods.ByName("RemovePath")),
connect.WithHandlerOptions(opts...),
)
hostAgentServiceCreateSnapshotHandler := connect.NewUnaryHandler(
HostAgentServiceCreateSnapshotProcedure,
svc.CreateSnapshot,
@ -551,6 +626,12 @@ func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.Han
hostAgentServiceWriteFileHandler.ServeHTTP(w, r)
case HostAgentServiceReadFileProcedure:
hostAgentServiceReadFileHandler.ServeHTTP(w, r)
case HostAgentServiceListDirProcedure:
hostAgentServiceListDirHandler.ServeHTTP(w, r)
case HostAgentServiceMakeDirProcedure:
hostAgentServiceMakeDirHandler.ServeHTTP(w, r)
case HostAgentServiceRemovePathProcedure:
hostAgentServiceRemovePathHandler.ServeHTTP(w, r)
case HostAgentServiceCreateSnapshotProcedure:
hostAgentServiceCreateSnapshotHandler.ServeHTTP(w, r)
case HostAgentServiceDeleteSnapshotProcedure:
@ -612,6 +693,18 @@ func (UnimplementedHostAgentServiceHandler) ReadFile(context.Context, *connect.R
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ReadFile is not implemented"))
}
func (UnimplementedHostAgentServiceHandler) ListDir(context.Context, *connect.Request[gen.ListDirRequest]) (*connect.Response[gen.ListDirResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ListDir is not implemented"))
}
func (UnimplementedHostAgentServiceHandler) MakeDir(context.Context, *connect.Request[gen.MakeDirRequest]) (*connect.Response[gen.MakeDirResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.MakeDir is not implemented"))
}
func (UnimplementedHostAgentServiceHandler) RemovePath(context.Context, *connect.Request[gen.RemovePathRequest]) (*connect.Response[gen.RemovePathResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.RemovePath is not implemented"))
}
func (UnimplementedHostAgentServiceHandler) CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.CreateSnapshot is not implemented"))
}

View File

@ -29,6 +29,15 @@ service HostAgentService {
// ReadFile reads a file from inside a sandbox.
rpc ReadFile(ReadFileRequest) returns (ReadFileResponse);
// ListDir lists directory contents inside a sandbox.
rpc ListDir(ListDirRequest) returns (ListDirResponse);
// MakeDir creates a directory inside a sandbox.
rpc MakeDir(MakeDirRequest) returns (MakeDirResponse);
// RemovePath removes a file or directory inside a sandbox.
rpc RemovePath(RemovePathRequest) returns (RemovePathResponse);
// CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable
// template, and destroys the sandbox.
rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse);
@ -269,6 +278,50 @@ message ReadFileStreamResponse {
bytes chunk = 1;
}
// ── Filesystem Operations ──────────────────────────────────────────
message ListDirRequest {
string sandbox_id = 1;
string path = 2;
uint32 depth = 3;
}
message ListDirResponse {
repeated FileEntry entries = 1;
}
message FileEntry {
string name = 1;
string path = 2;
// "file", "directory", or "symlink".
string type = 3;
int64 size = 4;
uint32 mode = 5;
// Human-readable permissions string, e.g. "-rwxr-xr-x".
string permissions = 6;
string owner = 7;
string group = 8;
// Last modification time as Unix timestamp (seconds).
int64 modified_at = 9;
optional string symlink_target = 10;
}
message MakeDirRequest {
string sandbox_id = 1;
string path = 2;
}
message MakeDirResponse {
FileEntry entry = 1;
}
message RemovePathRequest {
string sandbox_id = 1;
string path = 2;
}
message RemovePathResponse {}
// ── Ping ────────────────────────────────────────────────────────────
message PingSandboxRequest {