1
0
forked from wrenn/wrenn

Fix snapshot race, delete auth, sparse dd, default disk to 5GB

Snapshot race fix:
- Pre-mark sandbox as "paused" in DB before issuing CreateSnapshot and
  PauseSandbox RPCs, preventing the reconciler from marking it "stopped"
  during the flatten window when the sandbox is gone from the host
  agent's in-memory map but DB still says "running"
- Revert status to "running" on RPC failure
- Check ctx.Err() before writing response to avoid writing to dead
  connections when client disconnects during long snapshot operations

Delete auth fix:
- Block non-admin deletion of platform templates (team_id = all-zeros)
  at DELETE /v1/snapshots/{name} with 403, preventing file deletion
  before the team ownership check fails

Sparse dd:
- Add conv=sparse to dd in FlattenSnapshot so flattened images preserve
  sparseness (~200MB actual vs 5GB logical)

Default disk size:
- Change default disk_size_mb from 20GB to 5GB across migration,
  manager, service, build, and EnsureImageSizes
- Disable split-button dropdown arrow for platform templates in
  dashboard snapshots page (teams cannot delete platform templates)
This commit is contained in:
2026-03-28 14:30:18 +06:00
parent c89a664a37
commit 34af77e0d8
11 changed files with 89 additions and 42 deletions

View File

@ -54,6 +54,7 @@ export type Snapshot = {
memory_mb?: number;
size_bytes: number;
created_at: string;
platform: boolean;
};
export async function createSnapshot(sandboxId: string, name?: string): Promise<ApiResult<Snapshot>> {

View File

@ -423,6 +423,7 @@
<div class="w-px shrink-0 bg-[var(--color-border-mid)]"></div>
<!-- Chevron / dropdown trigger -->
<button
disabled={snapshot.platform}
onclick={(e) => {
e.stopPropagation();
if (openDropdownName === snapshot.name) {
@ -433,7 +434,7 @@
openDropdownName = snapshot.name;
}
}}
class="flex items-center px-2 py-1.5 text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-bright)]"
class="flex items-center px-2 py-1.5 text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-bright)] disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-[var(--color-text-secondary)]"
>
<svg
class="transition-transform duration-150 {openDropdownName === snapshot.name ? 'rotate-180' : ''}"
@ -484,21 +485,23 @@
class="fixed z-50 w-32 overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] py-1"
style="top: {dropdownPos.top}px; left: {dropdownPos.left}px; animation: fadeUp 0.15s ease both"
>
<button
onclick={(e) => {
e.stopPropagation();
const target = snapshots.find((s) => s.name === openDropdownName);
openDropdownName = null;
if (target) { deleteTarget = target; deleteError = null; }
}}
class="flex w-full items-center gap-2 px-3 py-2 text-meta text-[var(--color-red)] transition-colors duration-150 hover:bg-[var(--color-red)]/5"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
Delete
</button>
{#if !dropdownSnapshot.platform}
<button
onclick={(e) => {
e.stopPropagation();
const target = snapshots.find((s) => s.name === openDropdownName);
openDropdownName = null;
if (target) { deleteTarget = target; deleteError = null; }
}}
class="flex w-full items-center gap-2 px-3 py-2 text-meta text-[var(--color-red)] transition-colors duration-150 hover:bg-[var(--color-red)]/5"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
Delete
</button>
{/if}
</div>
{/if}
{/if}