Put the driver's sandbox status on the API instead of a projection

Fabro projected the driver's status into its own state enum, resource,
network, and timestamp types for the run sandbox and inventory endpoints,
losing the provider's state string, the network policy, the sandbox kind,
and the driver's own vocabulary along the way. The API now carries the
driver's SandboxStatus itself: SandboxDetails is fabro's run record beside
the status, SandboxInfo is the provider beside the status, and the
OpenAPI schema describes the driver's types (state, resources in the
units the driver reports, the network policy, the sandbox kind, workspace
ownership) which fabro-api reuses through with_replacement with round
trip tests proving identity and JSON parity. The projection types and
their conversion go; the web sandbox page and summary panel read the
status directly, and the TypeScript client is regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-11 13:07:00 -06:00
parent 7d9d2bf5f3
commit d24042ba64
No known key found for this signature in database
39 changed files with 907 additions and 1311 deletions

2
Cargo.lock generated
View file

@ -2327,6 +2327,7 @@ dependencies = [
"progenitor-client",
"regress",
"reqwest 0.13.4",
"sandbox-driver",
"serde",
"serde_json",
"serde_yaml",
@ -3247,6 +3248,7 @@ dependencies = [
"fabro-util",
"hex",
"lithos-llm",
"sandbox-driver",
"serde",
"serde_json",
"sha2 0.10.9",

View file

@ -174,7 +174,7 @@ describe("RunSummaryPanelView", () => {
const tree = render({
run: makeRun(),
sandboxState: "running",
sandboxResources: { cpu_cores: 4, memory_bytes: 8 * 1024 * 1024 * 1024 } as any,
sandboxResources: { cpu_cores: 4, memory_mb: 8 * 1024 },
});
expect(instanceText(cellAfterLabel(tree, "Sandbox"))).toBe("4 CPU · 8 GiB");
});

View file

@ -80,10 +80,10 @@ function SandboxValue({
}) {
const display = SANDBOX_STATE_DISPLAY[state] ?? SANDBOX_STATE_DISPLAY.unknown;
const cpu = resources?.cpu_cores;
const memory = resources?.memory_bytes;
const memoryMb = resources?.memory_mb;
const valueText =
cpu != null && memory != null
? `${formatCpuCores(cpu)} CPU · ${formatBytesAsMemory(memory)}`
cpu != null && memoryMb != null
? `${formatCpuCores(cpu)} CPU · ${formatBytesAsMemory(memoryMb * 1024 * 1024)}`
: display.label;
return (
@ -221,8 +221,8 @@ export function RunSummaryPanel({ runId }: { runId: string }) {
<RunSummaryPanelView
run={runQuery.data ?? null}
runLoading={runQuery.isLoading && !runQuery.data}
sandboxState={sandboxQuery.data?.state ?? null}
sandboxResources={sandboxQuery.data?.resources ?? null}
sandboxState={sandboxQuery.data?.status.state ?? null}
sandboxResources={sandboxQuery.data?.status.resources ?? null}
sandboxLoading={sandboxReady && sandboxQuery.isLoading && !sandboxQuery.data}
artifactsCount={artifactsQuery.data?.data.length ?? null}
artifactsLoading={artifactsQuery.isLoading && !artifactsQuery.data}

View file

@ -11,29 +11,31 @@ export interface SandboxStateDisplay {
text: string;
}
const PENDING = { dot: "bg-amber", text: "text-amber" } as const;
const QUIET = { dot: "bg-fg-muted", text: "text-fg-muted" } as const;
const GONE = { dot: "bg-coral", text: "text-coral" } as const;
/**
* Display metadata for every normalized sandbox lifecycle state. Shared by the
* Display metadata for every sandbox driver lifecycle state. Shared by the
* run overview summary panel and the dedicated sandbox page so the dot color,
* label, and hover copy stay consistent.
* label, and hover copy stay consistent. A state this build does not know
* renders as `unknown`.
*/
export const SANDBOX_STATE_DISPLAY: Record<SandboxState, SandboxStateDisplay> = {
unknown: {
label: "Unknown",
description: "The sandbox state could not be determined.",
dot: "bg-fg-muted",
text: "text-fg-muted",
...QUIET,
},
provisioning: {
label: "Provisioning",
description: "The sandbox is being provisioned.",
dot: "bg-amber",
text: "text-amber",
creating: {
label: "Creating",
description: "The sandbox is being created.",
...PENDING,
},
starting: {
label: "Starting",
description: "The sandbox is starting up.",
dot: "bg-amber",
text: "text-amber",
...PENDING,
},
running: {
label: "Running",
@ -44,55 +46,71 @@ export const SANDBOX_STATE_DISPLAY: Record<SandboxState, SandboxStateDisplay> =
stopping: {
label: "Stopping",
description: "The sandbox is shutting down.",
dot: "bg-amber",
text: "text-amber",
...PENDING,
},
stopped: {
label: "Stopped",
description: "The sandbox is stopped.",
dot: "bg-fg-muted",
text: "text-fg-muted",
...QUIET,
},
pausing: {
label: "Pausing",
description: "The sandbox is being paused.",
...PENDING,
},
paused: {
label: "Paused",
description: "The sandbox is paused.",
dot: "bg-amber",
text: "text-amber",
...PENDING,
},
deleting: {
label: "Deleting",
description: "The sandbox is being deleted.",
dot: "bg-amber",
text: "text-amber",
resuming: {
label: "Resuming",
description: "The sandbox is resuming.",
...PENDING,
},
deleted: {
label: "Deleted",
description: "The sandbox has been deleted.",
dot: "bg-coral",
text: "text-coral",
archiving: {
label: "Archiving",
description: "The sandbox is being archived.",
...PENDING,
},
archived: {
label: "Archived",
description: "The sandbox has been archived.",
dot: "bg-fg-muted",
text: "text-fg-muted",
...QUIET,
},
restoring: {
label: "Restoring",
description: "The sandbox is being restored.",
dot: "bg-amber",
text: "text-amber",
...PENDING,
},
resizing: {
label: "Resizing",
description: "The sandbox resources are being resized.",
dot: "bg-amber",
text: "text-amber",
...PENDING,
},
forking: {
label: "Forking",
description: "The sandbox is being forked.",
...PENDING,
},
snapshotting: {
label: "Snapshotting",
description: "A snapshot of the sandbox is being taken.",
...PENDING,
},
deleting: {
label: "Deleting",
description: "The sandbox is being deleted.",
...PENDING,
},
deleted: {
label: "Deleted",
description: "The sandbox has been deleted.",
...GONE,
},
error: {
label: "Error",
description: "The sandbox encountered an error.",
dot: "bg-coral",
text: "text-coral",
...GONE,
},
};

View file

@ -103,14 +103,15 @@ mock.restore();
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
function sandboxDetails(
overrides: Partial<SandboxDetails> & {
overrides: {
sandbox?: Partial<SandboxDetails["sandbox"]> & {
runtime?: Partial<NonNullable<SandboxDetails["sandbox"]["runtime"]>>;
};
status?: Partial<SandboxDetails["status"]>;
} = {},
): SandboxDetails {
const sandbox = overrides.sandbox ?? {};
const { sandbox: _sandboxOverride, ...detailOverrides } = overrides;
const status = overrides.status ?? {};
return {
sandbox: {
provider: "docker",
@ -126,34 +127,27 @@ function sandboxDetails(
},
...sandbox,
},
state: "running",
native_state: null,
region: null,
resources: { cpu_cores: null, memory_bytes: null, disk_bytes: null },
network: networkDetails(),
labels: {},
timestamps: { created_at: null, last_activity_at: null },
...detailOverrides,
status: {
id: sandbox.runtime?.id ?? "",
state: "running",
provider_state: "",
error_reason: null,
resources: null,
sandbox_kind: null,
region: null,
labels: {},
image: null,
snapshot: null,
network: null,
workspace_ownership: null,
web_url: null,
created_at: null,
updated_at: null,
...status,
},
};
}
function networkDetails(
overrides: Partial<SandboxDetails["network"]> = {},
): SandboxDetails["network"] {
return {
egress: networkPolicy("unknown"),
ingress: networkPolicy("unknown"),
...overrides,
};
}
function networkPolicy(
mode: SandboxDetails["network"]["egress"]["mode"],
cidrs: string[] = [],
): SandboxDetails["network"]["egress"] {
return { mode, cidrs };
}
function textContent(renderer: TestRenderer.ReactTestRenderer): string {
return renderer.root
.findAll((node) => typeof node.type === "string")
@ -230,22 +224,18 @@ describe("RunSandbox route", () => {
working_directory: "/workspace",
},
},
state: "running",
native_state: "running",
region: undefined,
resources: {
cpu_cores: 2,
memory_bytes: 4 * 1024 * 1024 * 1024,
disk_bytes: undefined,
},
network: networkDetails({
egress: networkPolicy("open"),
ingress: networkPolicy("blocked"),
}),
labels: { run: "abc" },
timestamps: {
created_at: "2026-05-09T12:00:00Z",
last_activity_at: undefined,
status: {
state: "running",
provider_state: "running",
resources: {
cpu_cores: 2,
memory_mb: 4 * 1024,
disk_mb: null,
gpus: null,
},
network: "allow_all",
labels: { run: "abc" },
created_at: "2026-05-09T12:00:00Z",
},
});
const renderer = renderRoute();
@ -256,8 +246,8 @@ describe("RunSandbox route", () => {
.filter((text): text is string => typeof text === "string");
expect(panelHeadings).toEqual(["Overview", "Resources", "Network", "Labels", "Timestamps"]);
const copy = textContent(renderer);
expect(copy).toContain("Open");
expect(copy).toContain("Blocked");
expect(copy).toContain("Allow all");
expect(copy).toContain("4 GiB");
});
test("links to the provider dashboard when a sandbox web URL is present", () => {
@ -269,8 +259,10 @@ describe("RunSandbox route", () => {
working_directory: "/workspace",
},
},
web_url:
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9",
status: {
web_url:
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9",
},
});
const renderer = renderRoute();
@ -296,18 +288,12 @@ describe("RunSandbox route", () => {
working_directory: "/tmp/project",
},
},
state: "unknown",
native_state: undefined,
region: undefined,
resources: {
cpu_cores: undefined,
memory_bytes: undefined,
disk_bytes: undefined,
},
labels: {},
timestamps: {
created_at: undefined,
last_activity_at: undefined,
status: {
state: "unknown",
resources: { cpu_cores: null, memory_mb: null, disk_mb: null, gpus: null },
labels: {},
created_at: null,
updated_at: null,
},
});
const renderer = renderRoute();
@ -328,35 +314,29 @@ describe("RunSandbox route", () => {
expect(noLabelsCopy).toHaveLength(1);
});
test("renders unknown network policies", () => {
currentDetails = sandboxDetails({
network: networkDetails({
egress: networkPolicy("unknown"),
ingress: networkPolicy("unknown"),
}),
});
test("renders an unknown network policy", () => {
currentDetails = sandboxDetails({ status: { network: null } });
const renderer = renderRoute();
const copy = textContent(renderer);
expect(copy).toContain("Network");
expect(copy).toContain("Egress");
expect(copy).toContain("Ingress");
expect(copy).toContain("Policy");
expect(copy).toContain("Unknown");
});
test("renders blocked, essentials, and CIDR network policies", () => {
test("renders blocked and CIDR allow list network policies", () => {
currentDetails = sandboxDetails({
network: networkDetails({
egress: networkPolicy("cidr_allow_list", ["10.0.0.0/8", "192.168.0.0/16"]),
ingress: networkPolicy("essentials_only"),
}),
status: { network: { cidr_allow_list: { cidrs: ["10.0.0.0/8", "192.168.0.0/16"] } } },
});
const renderer = renderRoute();
const copy = textContent(renderer);
expect(copy).toContain("CIDR allow list");
expect(copy).toContain("10.0.0.0/8, 192.168.0.0/16");
expect(copy).toContain("Essentials only");
currentDetails = sandboxDetails({ status: { network: "block" } });
const blocked = renderRoute();
expect(textContent(blocked)).toContain("Blocked");
});
test("shows the empty state when no sandbox is reported", () => {

View file

@ -22,7 +22,7 @@ import { SANDBOX_STATE_DISPLAY } from "../lib/sandbox-state";
import type {
RunSandbox,
SandboxDetails,
SandboxNetwork,
SandboxNetworkPolicy,
SandboxResources,
} from "@qltysh/fabro-api-client";
import FilesystemPanel from "./run-sandbox/filesystem-panel";
@ -57,27 +57,47 @@ function nullableTimestamp(value: string | null | undefined): string {
return value ? formatAbsoluteTs(value) : EMPTY_VALUE;
}
function nullableMemory(bytes: number | null | undefined): string {
return bytes != null ? formatBytesAsMemory(bytes) : EMPTY_VALUE;
function nullableMegabytes(megabytes: number | null | undefined): string {
return megabytes != null ? formatBytesAsMemory(megabytes * 1024 * 1024) : EMPTY_VALUE;
}
function nullableCpu(cores: number | null | undefined): string {
return cores != null ? formatCpuCores(cores) : EMPTY_VALUE;
}
type SandboxNetworkPolicy = SandboxNetwork["egress"];
type SandboxNetworkPolicyMode = SandboxNetworkPolicy["mode"];
function nullableCount(count: number | null | undefined): string {
return count != null ? String(count) : EMPTY_VALUE;
}
const NETWORK_POLICY_DISPLAY: Record<SandboxNetworkPolicyMode, string> = {
unknown: "Unknown",
open: "Open",
blocked: "Blocked",
cidr_allow_list: "CIDR allow list",
essentials_only: "Essentials only",
const NETWORK_POLICY_DISPLAY: Record<string, string> = {
provider_default: "Provider default",
allow_all: "Allow all",
block: "Blocked",
};
function networkPolicySummary(policy: SandboxNetworkPolicy): string {
return NETWORK_POLICY_DISPLAY[policy.mode] ?? policy.mode;
/** The policy's name, and the entries of an allow list when it carries one. */
function describeNetworkPolicy(
policy: SandboxNetworkPolicy | null | undefined,
): { summary: string; entries: { label: string; values: string[] } | null } {
if (policy == null) {
return { summary: "Unknown", entries: null };
}
if (typeof policy === "string") {
return { summary: NETWORK_POLICY_DISPLAY[policy] ?? policy, entries: null };
}
if ("cidr_allow_list" in policy) {
return {
summary: "CIDR allow list",
entries: { label: "Allowed CIDRs", values: policy.cidr_allow_list.cidrs },
};
}
if ("domain_allow_list" in policy) {
return {
summary: "Domain allow list",
entries: { label: "Allowed domains", values: policy.domain_allow_list.domains },
};
}
return { summary: "Unknown", entries: null };
}
interface RowProps {
@ -142,11 +162,12 @@ function Panel({ title, children }: PanelProps) {
}
function StatusStrip({ details }: { details: SandboxDetails }) {
const display = SANDBOX_STATE_DISPLAY[details.state] ?? SANDBOX_STATE_DISPLAY.unknown;
const status = details.status;
const display = SANDBOX_STATE_DISPLAY[status.state] ?? SANDBOX_STATE_DISPLAY.unknown;
const provider = details.sandbox.provider;
const providerState = status.provider_state ?? "";
const showNative =
details.native_state &&
details.native_state.toLowerCase() !== details.state.toLowerCase();
providerState.length > 0 && providerState.toLowerCase() !== status.state.toLowerCase();
return (
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 rounded-md border border-line bg-panel/60 px-4 py-3 text-sm">
<span className="font-mono text-xs text-fg-muted uppercase tracking-wide">
@ -158,7 +179,7 @@ function StatusStrip({ details }: { details: SandboxDetails }) {
</span>
{showNative && (
<span className="font-mono text-xs text-fg-muted">
({details.native_state})
({providerState})
</span>
)}
</div>
@ -167,20 +188,25 @@ function StatusStrip({ details }: { details: SandboxDetails }) {
function OverviewPanel({ details }: { details: SandboxDetails }) {
const sandbox = details.sandbox;
const status = details.status;
const runtime = sandbox.runtime;
return (
<Panel title="Overview">
<Row label="ID" value={nullable(runtime?.id)} />
<Row label="ID" value={nullable(status.id || runtime?.id)} />
<Row label="Working directory" value={nullable(runtime?.working_directory)} />
<Row
label="Region"
value={details.region ? details.region : sandbox.provider === "docker" ? "local" : EMPTY_VALUE}
value={status.region ? status.region : sandbox.provider === "docker" ? "local" : EMPTY_VALUE}
/>
<Row label="Image" value={nullable(sandbox.image ?? sandbox.snapshot)} />
{details.web_url && (
<Row
label="Image"
value={nullable(status.image ?? status.snapshot ?? sandbox.image ?? sandbox.snapshot)}
/>
{status.sandbox_kind && <Row label="Kind" value={status.sandbox_kind} />}
{status.web_url && (
<LinkRow
label="Provider"
href={details.web_url}
href={status.web_url}
text={
sandbox.provider === "daytona"
? "Open in Daytona"
@ -192,29 +218,25 @@ function OverviewPanel({ details }: { details: SandboxDetails }) {
);
}
function ResourcesPanel({ resources }: { resources: SandboxResources }) {
function ResourcesPanel({ resources }: { resources: SandboxResources | null | undefined }) {
return (
<Panel title="Resources">
<Row label="CPU" value={nullableCpu(resources.cpu_cores)} />
<Row label="Memory" value={nullableMemory(resources.memory_bytes)} />
<Row label="Disk" value={nullableMemory(resources.disk_bytes)} />
<Row label="CPU" value={nullableCpu(resources?.cpu_cores)} />
<Row label="Memory" value={nullableMegabytes(resources?.memory_mb)} />
<Row label="Disk" value={nullableMegabytes(resources?.disk_mb)} />
{resources?.gpus != null && <Row label="GPUs" value={nullableCount(resources.gpus)} />}
</Panel>
);
}
function NetworkPanel({ network }: { network: SandboxNetwork }) {
const cidrRows: Array<{ label: string; policy: SandboxNetworkPolicy }> = [
{ label: "Egress CIDRs", policy: network.egress },
{ label: "Ingress CIDRs", policy: network.ingress },
].filter(({ policy }) => policy.mode === "cidr_allow_list");
function NetworkPanel({ network }: { network: SandboxNetworkPolicy | null | undefined }) {
const { summary, entries } = describeNetworkPolicy(network);
return (
<Panel title="Network">
<Row label="Egress" value={networkPolicySummary(network.egress)} />
<Row label="Ingress" value={networkPolicySummary(network.ingress)} />
{cidrRows.map(({ label, policy }) => (
<Row key={label} label={label} value={policy.cidrs.join(", ") || EMPTY_VALUE} />
))}
<Row label="Policy" value={summary} />
{entries && (
<Row label={entries.label} value={entries.values.join(", ") || EMPTY_VALUE} />
)}
</Panel>
);
}
@ -237,11 +259,8 @@ function LabelsPanel({ labels }: { labels: { [key: string]: string } | null | un
function TimestampsPanel({ details }: { details: SandboxDetails }) {
return (
<Panel title="Timestamps">
<Row label="Created" value={nullableTimestamp(details.timestamps.created_at)} />
<Row
label="Last activity"
value={nullableTimestamp(details.timestamps.last_activity_at)}
/>
<Row label="Created" value={nullableTimestamp(details.status.created_at)} />
<Row label="Last updated" value={nullableTimestamp(details.status.updated_at)} />
</Panel>
);
}
@ -259,9 +278,9 @@ function DetailsColumn({ details }: { details: SandboxDetails | null }) {
<div className="space-y-4">
<StatusStrip details={details} />
<OverviewPanel details={details} />
<ResourcesPanel resources={details.resources} />
<NetworkPanel network={details.network} />
<LabelsPanel labels={details.labels} />
<ResourcesPanel resources={details.status.resources} />
<NetworkPanel network={details.status.network} />
<LabelsPanel labels={details.status.labels} />
<TimestampsPanel details={details} />
</div>
);

View file

@ -3907,7 +3907,7 @@ paths:
operationId: retrieveRunSandbox
tags: [Human-in-the-Loop]
summary: Retrieve Run Sandbox Details
description: Returns provider-neutral details about the sandbox owned by this run, including identity, normalized state, image/snapshot, resources, labels, and timestamps.
description: Returns the sandbox owned by this run as fabro's record of it plus the sandbox driver's status (identity, state, image or snapshot, resources, network policy, labels, and timestamps).
parameters:
- $ref: "#/components/parameters/RunId"
responses:
@ -13875,179 +13875,182 @@ components:
example: docker exec -it fabro-run-01HY0000000000000000000000 sh -lc 'cd /workspace/fabro && exec sh -l'
SandboxState:
description: Normalized sandbox lifecycle state used by the control plane and UI. The original provider-specific state string is preserved in `native_state`.
description: The sandbox driver's lifecycle state for a sandbox. The provider's own state string is preserved in `SandboxStatus.provider_state`. A reader must treat a value it does not know as `unknown`.
type: string
enum:
- unknown
- provisioning
- creating
- starting
- running
- stopping
- stopped
- pausing
- paused
- deleting
- deleted
- resuming
- archiving
- archived
- restoring
- resizing
- forking
- snapshotting
- deleting
- deleted
- error
- unknown
SandboxKind:
description: The kind of isolation a sandbox was provisioned with, as observed by the driver. Not an isolation guarantee.
type: string
enum:
- container
- virtual_machine
- unknown
SandboxWorkspaceOwnership:
description: Who owns a local sandbox's workspace directory. `designated` is a caller-owned directory that deleting the sandbox never touches; `managed` is a directory the driver created and removes.
type: string
enum:
- designated
- managed
SandboxResources:
description: Resource configuration for a sandbox. Fields are nullable when the provider does not surface a value or no limit is configured.
description: Compute resources of a sandbox, in the units the field names give. A field is null when the provider does not report a value or applies its default.
type: object
properties:
cpu_cores:
type: number
format: double
description: Configured CPU cores. Null when unavailable.
memory_bytes:
type: integer
type: ["integer", "null"]
format: int64
minimum: 0
description: Memory limit in bytes. Null when unavailable or unlimited.
disk_bytes:
type: integer
memory_mb:
type: ["integer", "null"]
format: int64
minimum: 0
disk_mb:
type: ["integer", "null"]
format: int64
minimum: 0
gpus:
type: ["integer", "null"]
format: int64
minimum: 0
description: Disk size in bytes. Null when unavailable.
SandboxNetworkPolicyMode:
description: Provider-neutral public-network policy for one direction.
type: string
enum:
- unknown
- open
- blocked
- cidr_allow_list
- essentials_only
SandboxNetworkPolicy:
description: Public-network policy for one direction.
description: The network policy in force for a sandbox. A policy without parameters is its name; an allow list carries its entries.
oneOf:
- type: string
enum:
- provider_default
- allow_all
- block
- type: object
required: [cidr_allow_list]
properties:
cidr_allow_list:
type: object
required: [cidrs]
properties:
cidrs:
type: array
items:
type: string
- type: object
required: [domain_allow_list]
properties:
domain_allow_list:
type: object
required: [domains]
properties:
domains:
type: array
items:
type: string
SandboxStatus:
description: What the sandbox driver reports about a sandbox. Only `id` and `state` are always present; every other field is null or empty when the provider does not report it.
type: object
required:
- mode
- cidrs
- id
- state
properties:
mode:
$ref: "#/components/schemas/SandboxNetworkPolicyMode"
cidrs:
type: array
items:
id:
type: string
description: The provider's stable identifier for the sandbox.
name:
type: ["string", "null"]
description: The provider's display name, which is not the stable identifier.
state:
$ref: "#/components/schemas/SandboxState"
provider_state:
type: string
default: ""
description: The provider's own state string, for display and debugging.
error_reason:
type: ["string", "null"]
resources:
oneOf:
- $ref: "#/components/schemas/SandboxResources"
- type: "null"
sandbox_kind:
oneOf:
- $ref: "#/components/schemas/SandboxKind"
- type: "null"
region:
type: ["string", "null"]
description: The provider region or target the sandbox runs in.
labels:
type: object
additionalProperties:
type: string
description: CIDR entries when `mode` is `cidr_allow_list`; empty for other modes.
SandboxNetwork:
description: Provider-neutral public-network policy for sandbox egress and ingress.
type: object
required:
- egress
- ingress
properties:
egress:
$ref: "#/components/schemas/SandboxNetworkPolicy"
ingress:
$ref: "#/components/schemas/SandboxNetworkPolicy"
SandboxTimestamps:
description: Lifecycle timestamps for a sandbox. Fields are nullable when the provider does not surface a value.
type: object
properties:
description: Provider-stored labels, including fabro's ownership labels.
image:
type: ["string", "null"]
description: The image the sandbox runs, when the provider knows it (a Docker container's image reference).
snapshot:
type: ["string", "null"]
description: The snapshot the sandbox was created from, when the provider knows it (a Daytona snapshot name).
network:
oneOf:
- $ref: "#/components/schemas/SandboxNetworkPolicy"
- type: "null"
description: The network policy in force, when the provider can read it back.
workspace_ownership:
oneOf:
- $ref: "#/components/schemas/SandboxWorkspaceOwnership"
- type: "null"
description: Local sandboxes only.
web_url:
type: ["string", "null"]
description: The provider's console page for the sandbox, when it has one.
created_at:
type: string
type: ["string", "null"]
format: date-time
description: When the sandbox was created.
last_activity_at:
type: string
updated_at:
type: ["string", "null"]
format: date-time
description: Most recent activity timestamp reported by the provider.
description: The provider's most recent activity or update timestamp for the sandbox.
SandboxDetails:
description: Provider-neutral details about the sandbox owned by a run.
description: The sandbox owned by a run, as fabro's record of it and the sandbox driver's status.
type: object
required:
- sandbox
- state
- resources
- network
- labels
- timestamps
- status
properties:
sandbox:
$ref: "#/components/schemas/RunSandboxInstance"
state:
$ref: "#/components/schemas/SandboxState"
native_state:
type: ["string", "null"]
description: Original provider state string before normalization. Display/debugging only; UI behavior keys off `state`.
region:
type: ["string", "null"]
description: Provider region or target. Null for local-style providers.
web_url:
type: ["string", "null"]
description: Provider dashboard URL for this sandbox when available.
resources:
$ref: "#/components/schemas/SandboxResources"
network:
$ref: "#/components/schemas/SandboxNetwork"
labels:
type: object
additionalProperties:
type: string
description: Provider-reported labels.
timestamps:
$ref: "#/components/schemas/SandboxTimestamps"
status:
$ref: "#/components/schemas/SandboxStatus"
SandboxInfo:
description: Provider-backed inventory record for a Fabro-managed sandbox.
description: One sandbox of fabro's provider-backed inventory, as the provider fabro connected it through and the sandbox driver's status.
type: object
required:
- provider
- id
- state
- resources
- network
- labels
- timestamps
- status
properties:
provider:
$ref: "#/components/schemas/SandboxProviderKind"
id:
type: string
description: Provider-native sandbox id.
display_name:
type: ["string", "null"]
description: Provider display name when distinct from the native id.
state:
$ref: "#/components/schemas/SandboxState"
native_state:
type: ["string", "null"]
description: Original provider state string before normalization. Display/debugging only; UI behavior keys off `state`.
image:
type: ["string", "null"]
description: Provider image when surfaced by the sandbox provider.
snapshot:
type: ["string", "null"]
description: Provider snapshot when surfaced by the sandbox provider.
region:
type: ["string", "null"]
description: Provider region or target. Null for local-style providers.
web_url:
type: ["string", "null"]
description: Provider dashboard URL for this sandbox when available.
working_directory:
type: ["string", "null"]
description: Provider-reported or Fabro-default working directory when available.
resources:
$ref: "#/components/schemas/SandboxResources"
network:
$ref: "#/components/schemas/SandboxNetwork"
labels:
type: object
additionalProperties:
type: string
description: Provider-reported labels.
timestamps:
$ref: "#/components/schemas/SandboxTimestamps"
status:
$ref: "#/components/schemas/SandboxStatus"
SandboxProviderLookupError:
description: Provider error captured during fail-soft sandbox inventory lookup.

View file

@ -1325,6 +1325,17 @@ mod retrieve_sandbox_tests {
run_store: &fabro_store::RunDatabase,
run_id: &RunId,
provider: &str,
) {
append_sandbox_initialized_in(run_store, run_id, provider, "/workspace").await;
}
/// A local sandbox reconnects by attaching to its working directory, so
/// a test that reaches one records a directory that exists.
async fn append_sandbox_initialized_in(
run_store: &fabro_store::RunDatabase,
run_id: &RunId,
provider: &str,
working_directory: &str,
) {
let payload = fabro_store::EventPayload::new(
json!({
@ -1335,7 +1346,7 @@ mod retrieve_sandbox_tests {
"properties": {
"provider": provider,
"id": format!("{provider}:sandbox-id"),
"working_directory": "/workspace",
"working_directory": working_directory,
},
}),
run_id,
@ -1469,7 +1480,11 @@ mod retrieve_sandbox_tests {
.await
.expect("test run should be creatable");
append_run_created(&run_store, &run_id).await;
append_sandbox_initialized(&run_store, &run_id, "local").await;
// A record written before local sandboxes had directory-derived
// ids: the id is recomputed from the directory on reconnect.
let workspace = tempfile::tempdir().expect("scratch directory");
let working_directory = workspace.path().to_str().expect("utf-8").to_owned();
append_sandbox_initialized_in(&run_store, &run_id, "local", &working_directory).await;
let response = app
.oneshot(req_get(&format!("/api/v1/runs/{run_id}/sandbox")))
@ -1481,15 +1496,19 @@ mod retrieve_sandbox_tests {
assert_eq!(body["sandbox"]["runtime"]["id"], "local:sandbox-id");
assert_eq!(
body["sandbox"]["runtime"]["working_directory"],
"/workspace"
working_directory
);
assert_eq!(body["state"], "running");
assert!(body.get("name").is_none());
assert_eq!(body["status"]["state"], "running");
assert_eq!(body["status"]["workspace_ownership"], "designated");
assert!(
body["status"]["id"]
.as_str()
.is_some_and(|id| id.starts_with("host-dir-")),
"{}",
body["status"]["id"]
);
assert!(body.get("state").is_none(), "the status is not flattened");
assert!(body.get("identifier").is_none());
assert!(body["resources"].is_object());
assert_eq!(body["network"]["egress"]["mode"], "unknown");
assert_eq!(body["network"]["ingress"]["mode"], "unknown");
assert!(body["timestamps"].is_object());
}
#[tokio::test]
@ -1503,7 +1522,14 @@ mod retrieve_sandbox_tests {
.await
.expect("test run should be creatable");
append_run_created(&run_store, &run_id).await;
append_sandbox_initialized(&run_store, &run_id, "local").await;
let workspace = tempfile::tempdir().expect("scratch directory");
append_sandbox_initialized_in(
&run_store,
&run_id,
"local",
workspace.path().to_str().expect("utf-8"),
)
.await;
let response = app
.oneshot(req_post(&format!("/api/v1/runs/{run_id}/sandbox/vnc")))

View file

@ -148,9 +148,9 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
let body = body_json(response).await;
assert_eq!(body["data"][0]["id"], "docker-native-id");
assert_eq!(body["data"][0]["status"]["id"], "docker-native-id");
assert_eq!(body["data"][0]["provider"], "docker");
assert_eq!(body["data"][0]["state"], "running");
assert_eq!(body["data"][0]["status"]["state"], "running");
assert_eq!(body["meta"]["provider_errors"], json!([]));
}
@ -169,7 +169,7 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
let body = body_json(response).await;
assert_eq!(body["id"], "native-id");
assert_eq!(body["status"]["id"], "native-id");
assert_eq!(body["provider"], "daytona");
}

View file

@ -1,16 +1,11 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use fabro_types::{
RunId, RunSandboxInstance, SandboxDetails, SandboxNetwork, SandboxResources, SandboxState,
SandboxTimestamps,
};
use fabro_types::{RunId, RunSandboxInstance, SandboxDetails};
use crate::driver::ProviderAccess;
use crate::reconnect;
/// Inspect the sandbox identified by `record` and return provider-neutral
/// details for control-plane display, described through the sandbox driver
/// on every provider.
/// The sandbox identified by `record`, as the run record fabro keeps and
/// the status the sandbox driver reports for it, on every provider.
pub async fn sandbox_details(
record: &RunSandboxInstance,
access: &ProviderAccess,
@ -24,185 +19,8 @@ pub async fn sandbox_details(
record.runtime.id
)
})?;
Ok(details_from_status(record, &status))
}
/// Projection of a sandbox-driver [`sandbox_driver::SandboxStatus`] into
/// fabro's inventory shape. The driver reports what a provider exposes
/// through its public facets; fields no facet carries (network policy) stay
/// unknown rather than being read from provider SDK types.
pub(crate) fn info_from_status(
kind: &fabro_types::SandboxProviderKind,
status: &sandbox_driver::SandboxStatus,
) -> fabro_types::SandboxInfo {
let fields = fields_from_status(status);
fabro_types::SandboxInfo {
provider: kind.clone(),
id: status.id.to_string(),
display_name: status.name.clone().filter(|name| !name.is_empty()),
state: fields.state,
native_state: fields.native_state,
image: status.image.clone(),
snapshot: status.snapshot.clone(),
region: status.region.clone(),
web_url: status.web_url.clone(),
working_directory: None,
resources: fields.resources,
network: SandboxNetwork::unknown(),
labels: status.labels.clone(),
timestamps: fields.timestamps,
}
}
pub(crate) fn details_from_status(
record: &RunSandboxInstance,
status: &sandbox_driver::SandboxStatus,
) -> SandboxDetails {
let fields = fields_from_status(status);
SandboxDetails {
sandbox: RunSandboxInstance {
image: status.image.clone().or_else(|| record.image.clone()),
snapshot: status.snapshot.clone().or_else(|| record.snapshot.clone()),
..record.clone()
},
state: fields.state,
native_state: fields.native_state,
region: status.region.clone(),
web_url: status.web_url.clone(),
resources: fields.resources,
network: SandboxNetwork::unknown(),
labels: status.labels.clone(),
timestamps: fields.timestamps,
}
}
struct StatusFields {
state: SandboxState,
native_state: Option<String>,
resources: SandboxResources,
timestamps: SandboxTimestamps,
}
fn fields_from_status(status: &sandbox_driver::SandboxStatus) -> StatusFields {
StatusFields {
state: normalize_driver_state(status.state),
native_state: Some(status.provider_state.clone()).filter(|value| !value.is_empty()),
resources: status
.resources
.as_ref()
.map(|resources| SandboxResources {
cpu_cores: resources.cpu_cores.map(f64::from),
memory_bytes: resources.memory_mb.map(|mb| mb * 1024 * 1024),
disk_bytes: resources.disk_mb.map(|mb| mb * 1024 * 1024),
})
.unwrap_or_default(),
timestamps: SandboxTimestamps {
created_at: status.created_at.map(DateTime::<Utc>::from),
last_activity_at: status.updated_at.map(DateTime::<Utc>::from),
},
}
}
pub(crate) fn normalize_driver_state(state: sandbox_driver::SandboxState) -> SandboxState {
use sandbox_driver::SandboxState as Driver;
match state {
Driver::Creating | Driver::Forking => SandboxState::Provisioning,
Driver::Starting | Driver::Resuming => SandboxState::Starting,
// A sandbox mid-snapshot keeps serving commands.
Driver::Running | Driver::Snapshotting => SandboxState::Running,
Driver::Stopping | Driver::Archiving => SandboxState::Stopping,
Driver::Stopped => SandboxState::Stopped,
Driver::Pausing | Driver::Paused => SandboxState::Paused,
Driver::Archived => SandboxState::Archived,
Driver::Restoring => SandboxState::Restoring,
Driver::Resizing => SandboxState::Resizing,
Driver::Deleting => SandboxState::Deleting,
Driver::Deleted => SandboxState::Deleted,
Driver::Error => SandboxState::Error,
_ => SandboxState::Unknown,
}
}
#[cfg(test)]
mod tests {
use fabro_types::SandboxProviderKind;
use sandbox_driver::SandboxId;
use super::*;
#[test]
fn driver_states_map_onto_fabro_states() {
use sandbox_driver::SandboxState as Driver;
for (driver, fabro) in [
(Driver::Creating, SandboxState::Provisioning),
(Driver::Starting, SandboxState::Starting),
(Driver::Running, SandboxState::Running),
(Driver::Snapshotting, SandboxState::Running),
(Driver::Stopping, SandboxState::Stopping),
(Driver::Stopped, SandboxState::Stopped),
(Driver::Paused, SandboxState::Paused),
(Driver::Archived, SandboxState::Archived),
(Driver::Deleting, SandboxState::Deleting),
(Driver::Deleted, SandboxState::Deleted),
(Driver::Error, SandboxState::Error),
(Driver::Unknown, SandboxState::Unknown),
] {
assert_eq!(normalize_driver_state(driver), fabro, "{driver:?}");
}
}
#[test]
fn status_projection_carries_identity_source_and_labels() {
let mut status = sandbox_driver::SandboxStatus::new(
SandboxId::try_new("container-abc123").unwrap(),
sandbox_driver::SandboxState::Running,
);
status.name = Some("fabro-run-abc".to_string());
status.provider_state = "running".to_string();
status.image = Some("buildpack-deps:noble".to_string());
status
.labels
.insert("sh.fabro.managed".to_string(), "true".to_string());
let mut resources = sandbox_driver::Resources::default();
resources.cpu_cores = Some(2);
resources.memory_mb = Some(2048);
status.resources = Some(resources);
let info = info_from_status(&SandboxProviderKind::DOCKER, &status);
assert_eq!(info.id, "container-abc123");
assert_eq!(info.display_name.as_deref(), Some("fabro-run-abc"));
assert_eq!(info.state, SandboxState::Running);
assert_eq!(info.native_state.as_deref(), Some("running"));
assert_eq!(info.image.as_deref(), Some("buildpack-deps:noble"));
assert_eq!(info.resources.cpu_cores, Some(2.0));
assert_eq!(info.resources.memory_bytes, Some(2_147_483_648));
assert_eq!(
info.labels.get("sh.fabro.managed").map(String::as_str),
Some("true")
);
let record = RunSandboxInstance {
provider: SandboxProviderKind::DOCKER,
image: None,
snapshot: None,
runtime: fabro_types::RunSandboxRuntime {
id: "container-abc123".to_string(),
working_directory: "/workspace".to_string(),
repo_cloned: Some(true),
clone_origin_url: None,
clone_branch: None,
workspace_root: None,
repos_root: None,
primary_repo_path: None,
primary_repo_link: None,
},
};
let details = details_from_status(&record, &status);
assert_eq!(
details.sandbox.image.as_deref(),
Some("buildpack-deps:noble")
);
assert_eq!(details.sandbox.runtime.id, "container-abc123");
assert_eq!(details.network, SandboxNetwork::unknown());
}
Ok(SandboxDetails {
sandbox: record.clone(),
status,
})
}

View file

@ -26,7 +26,7 @@ use sandbox_driver::{
use tokio::sync::OnceCell;
use crate::driver::{ConnectedProvider, ProviderConnectOptions, connect_provider};
use crate::{details, managed_labels};
use crate::managed_labels;
/// The sandboxes fabro manages, by provider.
#[derive(Clone, Default)]
@ -206,8 +206,11 @@ impl InventoryEntry {
crate::Error::context(format!("Failed to list {} sandboxes", self.kind), error)
})?;
Ok(statuses
.iter()
.map(|status| details::info_from_status(&self.kind, status))
.into_iter()
.map(|status| SandboxInfo {
provider: self.kind.clone(),
status,
})
.collect())
}
@ -240,7 +243,10 @@ impl InventoryEntry {
if status.state == SandboxState::Deleted {
return Ok(None);
}
Ok(Some(details::info_from_status(&self.kind, &status)))
Ok(Some(SandboxInfo {
provider: self.kind.clone(),
status,
}))
}
}
@ -330,7 +336,7 @@ mod tests {
let response = inventory.list_managed().await;
let mut ids: Vec<_> = response.data.iter().map(|s| s.id.as_str()).collect();
let mut ids: Vec<_> = response.data.iter().map(|s| s.status.id.as_str()).collect();
ids.sort_unstable();
assert_eq!(ids, ["daytona-1", "docker-1"]);
assert!(response.meta.provider_errors.is_empty());
@ -375,7 +381,7 @@ mod tests {
.await
.expect("one provider matches");
assert_eq!(sandbox.id, "native-id");
assert_eq!(sandbox.status.id.as_str(), "native-id");
assert_eq!(sandbox.provider, SandboxProviderKind::DAYTONA);
}

View file

@ -23,6 +23,7 @@ lithos-llm = { workspace = true, features = ["runtime"] }
progenitor-client = "0.13"
regress = "0.10"
reqwest.workspace = true
sandbox-driver.workspace = true
serde.workspace = true
serde_json.workspace = true
uuid = { workspace = true, features = ["serde"] }

View file

@ -657,15 +657,17 @@ fn main() {
"fabro_types::SandboxListResponse",
&[],
),
("SandboxNetwork", "fabro_types::SandboxNetwork", &[]),
// A sandbox's status is the sandbox driver's own type: the API reuses
// it and the types it carries rather than projecting them.
("SandboxStatus", "sandbox_driver::SandboxStatus", &[]),
("SandboxId", "sandbox_driver::SandboxId", &[]),
("SandboxState", "sandbox_driver::SandboxState", &[]),
("SandboxResources", "sandbox_driver::Resources", &[]),
("SandboxNetworkPolicy", "sandbox_driver::NetworkPolicy", &[]),
("SandboxKind", "sandbox_driver::SandboxKind", &[]),
(
"SandboxNetworkPolicy",
"fabro_types::SandboxNetworkPolicy",
&[],
),
(
"SandboxNetworkPolicyMode",
"fabro_types::SandboxNetworkPolicyMode",
"SandboxWorkspaceOwnership",
"sandbox_driver::WorkspaceOwnership",
&[],
),
("SandboxService", "fabro_types::SandboxService", &[]),

View file

@ -61,19 +61,18 @@ pub mod types {
RunPairStatusResponse, RunProjection, RunProvenance, RunRunnableSource, RunSandbox,
RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan, RunSandboxRuntime,
RunServerProvenance, RunSize, RunTarget, SandboxDetails, SandboxInfo, SandboxListMeta,
SandboxListResponse, SandboxNetwork, SandboxNetworkPolicy, SandboxNetworkPolicyMode,
SandboxProviderKind, SandboxProviderLookupError, SandboxResources, SandboxService,
SandboxServiceListResponse, SandboxState, SandboxTimestamps, SecretMetadata, SecretType,
ServerSettings, SessionDetail, SessionId, SessionMessage, SessionRecord, SessionStatus,
SessionSummary, SessionTurn, SkillsProjection, StageCompletion, StageContextWindow,
StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod,
StageContextWindowProjection, StageContextWindowStaleness,
StageContextWindowUnavailableReason, StageContextWindowWarning, StageHandler, StageId,
StageInferenceProjection, StageModelUsage, StageOutcome, StageProjection, StageState,
StageToolBatchProjection, SubAgentProjection, SubAgentStatus, SystemActorKind,
SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection, TurnId,
UpdateVariableRequest, UserPrincipal, Variable, VariableListResponse, WorkflowPath,
WorkflowSettings, WorkflowVersion, WorkflowVersionId,
SandboxListResponse, SandboxProviderKind, SandboxProviderLookupError, SandboxService,
SandboxServiceListResponse, SecretMetadata, SecretType, ServerSettings, SessionDetail,
SessionId, SessionMessage, SessionRecord, SessionStatus, SessionSummary, SessionTurn,
SkillsProjection, StageCompletion, StageContextWindow, StageContextWindowBreakdownItem,
StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection,
StageContextWindowStaleness, StageContextWindowUnavailableReason,
StageContextWindowWarning, StageHandler, StageId, StageInferenceProjection,
StageModelUsage, StageOutcome, StageProjection, StageState, StageToolBatchProjection,
SubAgentProjection, SubAgentStatus, SystemActorKind, SystemIntegrationStatus,
SystemIntegrationsResponse, TodoListProjection, TurnId, UpdateVariableRequest,
UserPrincipal, Variable, VariableListResponse, WorkflowPath, WorkflowSettings,
WorkflowVersion, WorkflowVersionId,
};
pub use lithos_llm::catalog::{ModelHandle, ProviderId};
pub use lithos_llm::types::{
@ -83,6 +82,11 @@ pub mod types {
ToolDefinition as CompletionToolDefinition,
ToolDefinitionKind as CompletionToolDefinitionKind,
};
/// A sandbox's status on the API is the sandbox driver's own type.
pub use sandbox_driver::{
NetworkPolicy as SandboxNetworkPolicy, Resources as SandboxResources, SandboxId,
SandboxKind, SandboxState, SandboxStatus, WorkspaceOwnership as SandboxWorkspaceOwnership,
};
pub use crate::generated::types::*;
}

View file

@ -1,38 +1,60 @@
use std::any::{TypeId, type_name};
use std::collections::BTreeMap;
use std::time::SystemTime;
use chrono::{TimeZone, Utc};
use chrono::DateTime;
use fabro_api::types::{
SandboxDetails as ApiSandboxDetails, SandboxNetwork as ApiSandboxNetwork,
SandboxNetworkPolicy as ApiSandboxNetworkPolicy,
SandboxNetworkPolicyMode as ApiSandboxNetworkPolicyMode,
SandboxProviderKind as ApiSandboxProvider, SandboxResources as ApiSandboxResources,
SandboxState as ApiSandboxState, SandboxTimestamps as ApiSandboxTimestamps,
SandboxDetails as ApiSandboxDetails, SandboxId as ApiSandboxId, SandboxKind as ApiSandboxKind,
SandboxNetworkPolicy as ApiSandboxNetworkPolicy, SandboxProviderKind as ApiSandboxProvider,
SandboxResources as ApiSandboxResources, SandboxState as ApiSandboxState,
SandboxStatus as ApiSandboxStatus, SandboxWorkspaceOwnership as ApiSandboxWorkspaceOwnership,
};
use fabro_types::{
RunSandboxInstance, RunSandboxRuntime, SandboxDetails, SandboxNetwork, SandboxNetworkPolicy,
SandboxNetworkPolicyMode, SandboxProviderKind, SandboxResources, SandboxState,
SandboxTimestamps,
use fabro_types::{RunSandboxInstance, RunSandboxRuntime, SandboxDetails, SandboxProviderKind};
use sandbox_driver::{
NetworkPolicy, Resources, SandboxId, SandboxKind, SandboxState, SandboxStatus,
WorkspaceOwnership,
};
use serde_json::json;
#[test]
fn sandbox_details_reuses_domain_types() {
fn sandbox_details_reuses_the_domain_and_driver_types() {
assert_same_type::<ApiSandboxDetails, SandboxDetails>();
assert_same_type::<ApiSandboxProvider, SandboxProviderKind>();
assert_same_type::<ApiSandboxStatus, SandboxStatus>();
assert_same_type::<ApiSandboxId, SandboxId>();
assert_same_type::<ApiSandboxState, SandboxState>();
assert_same_type::<ApiSandboxResources, SandboxResources>();
assert_same_type::<ApiSandboxTimestamps, SandboxTimestamps>();
assert_same_type::<ApiSandboxNetwork, SandboxNetwork>();
assert_same_type::<ApiSandboxNetworkPolicy, SandboxNetworkPolicy>();
assert_same_type::<ApiSandboxNetworkPolicyMode, SandboxNetworkPolicyMode>();
assert_same_type::<ApiSandboxResources, Resources>();
assert_same_type::<ApiSandboxNetworkPolicy, NetworkPolicy>();
assert_same_type::<ApiSandboxKind, SandboxKind>();
assert_same_type::<ApiSandboxWorkspaceOwnership, WorkspaceOwnership>();
}
#[test]
fn sandbox_details_json_matches_openapi_shape() {
let created_at = Utc.with_ymd_and_hms(2026, 5, 9, 12, 0, 0).unwrap();
let mut status = SandboxStatus::new(
SandboxId::try_new("container-abc123").unwrap(),
SandboxState::Running,
);
status.name = Some("fabro-run-abc".to_string());
status.provider_state = "running".to_string();
let mut resources = Resources::default();
resources.cpu_cores = Some(2);
resources.memory_mb = Some(4096);
status.resources = Some(resources);
status.sandbox_kind = Some(SandboxKind::Container);
status.labels.insert("run".to_string(), "abc".to_string());
status.image = Some("ghcr.io/fabro/sandbox:latest".to_string());
status.network = Some(NetworkPolicy::CidrAllowList {
cidrs: vec!["10.0.0.0/8".to_string()],
});
status.web_url = Some(
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9"
.to_string(),
);
status.created_at = Some(SystemTime::from(
DateTime::parse_from_rfc3339("2026-05-09T12:00:00Z").unwrap(),
));
let details = SandboxDetails {
sandbox: RunSandboxInstance {
sandbox: RunSandboxInstance {
provider: SandboxProviderKind::DOCKER,
image: Some("ghcr.io/fabro/sandbox:latest".to_string()),
snapshot: None,
@ -48,27 +70,7 @@ fn sandbox_details_json_matches_openapi_shape() {
primary_repo_link: Some("/workspace/fabro".to_string()),
},
},
state: SandboxState::Running,
native_state: Some("running".to_string()),
region: None,
web_url: Some(
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9"
.to_string(),
),
resources: SandboxResources {
cpu_cores: Some(2.0),
memory_bytes: Some(4 * 1024 * 1024 * 1024),
disk_bytes: None,
},
network: SandboxNetwork {
egress: SandboxNetworkPolicy::open(),
ingress: SandboxNetworkPolicy::blocked(),
},
labels: BTreeMap::from([("run".to_string(), "abc".to_string())]),
timestamps: SandboxTimestamps {
created_at: Some(created_at),
last_activity_at: None,
},
status,
};
assert_eq!(
@ -86,75 +88,68 @@ fn sandbox_details_json_matches_openapi_shape() {
"primary_repo_link": "/workspace/fabro"
}
},
"state": "running",
"native_state": "running",
"web_url": "https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9",
"resources": {
"cpu_cores": 2.0,
"memory_bytes": 4_294_967_296_u64,
},
"network": {
"egress": {
"mode": "open",
"cidrs": []
"status": {
"id": "container-abc123",
"name": "fabro-run-abc",
"state": "running",
"provider_state": "running",
"error_reason": null,
"resources": {
"cpu_cores": 2,
"memory_mb": 4096,
"disk_mb": null,
"gpus": null
},
"ingress": {
"mode": "blocked",
"cidrs": []
}
},
"labels": {
"run": "abc"
},
"timestamps": {
"created_at": "2026-05-09T12:00:00Z"
"sandbox_kind": "container",
"region": null,
"labels": { "run": "abc" },
"image": "ghcr.io/fabro/sandbox:latest",
"snapshot": null,
"network": { "cidr_allow_list": { "cidrs": ["10.0.0.0/8"] } },
"workspace_ownership": null,
"web_url": "https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9",
"created_at": "2026-05-09T12:00:00Z",
"updated_at": null
}
})
);
}
#[test]
fn sandbox_details_deserializes_when_optional_fields_are_absent() {
fn sandbox_details_deserializes_a_status_with_only_its_required_fields() {
let details: SandboxDetails = serde_json::from_value(json!({
"sandbox": {
"provider": "local",
"runtime": {
"id": "local:01JNQVR7M0EJ5GKAT2SC4ERS1Z",
"id": "host-dir-2f55736572732f636c69656e742f70726f6a656374",
"working_directory": "/Users/client/project"
}
},
"state": "unknown",
"resources": {},
"labels": {},
"timestamps": {}
"status": {
"id": "host-dir-2f55736572732f636c69656e742f70726f6a656374",
"state": "running",
"workspace_ownership": "designated"
}
}))
.unwrap();
assert_eq!(details.sandbox.provider, SandboxProviderKind::LOCAL);
assert_eq!(details.status.state, SandboxState::Running);
assert_eq!(
details.sandbox.runtime.id.as_str(),
"local:01JNQVR7M0EJ5GKAT2SC4ERS1Z"
details.status.workspace_ownership,
Some(WorkspaceOwnership::Designated)
);
assert_eq!(
details.sandbox.runtime.working_directory.as_str(),
"/Users/client/project"
);
assert_eq!(details.state, SandboxState::Unknown);
assert!(details.sandbox.image.is_none());
assert!(details.region.is_none());
assert!(details.native_state.is_none());
assert!(details.labels.is_empty());
assert_eq!(details.resources, SandboxResources::default());
assert_eq!(details.network, SandboxNetwork::unknown());
assert_eq!(details.timestamps, SandboxTimestamps::default());
assert!(details.status.resources.is_none());
assert!(details.status.network.is_none());
assert!(details.status.created_at.is_none());
}
fn assert_same_type<T: 'static, U: 'static>() {
fn assert_same_type<A: 'static, B: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
TypeId::of::<A>(),
TypeId::of::<B>(),
"{} should be {}",
type_name::<A>(),
type_name::<B>()
);
}

View file

@ -1,17 +1,17 @@
use std::any::{TypeId, type_name};
use std::collections::BTreeMap;
use std::time::SystemTime;
use chrono::{TimeZone, Utc};
use chrono::DateTime;
use fabro_api::types::{
SandboxInfo as ApiSandboxInfo, SandboxListMeta as ApiSandboxListMeta,
SandboxListResponse as ApiSandboxListResponse, SandboxProviderKind as ApiSandboxProviderKind,
SandboxProviderLookupError as ApiSandboxProviderLookupError,
};
use fabro_types::{
SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxNetwork, SandboxNetworkPolicy,
SandboxProviderKind, SandboxProviderLookupError, SandboxResources, SandboxState,
SandboxTimestamps,
SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxProviderKind,
SandboxProviderLookupError,
};
use sandbox_driver::{NetworkPolicy, Resources, SandboxId, SandboxState, SandboxStatus};
use serde_json::json;
#[test]
@ -25,99 +25,95 @@ fn sandbox_inventory_round_trip_reuses_domain_types() {
#[test]
fn sandbox_inventory_round_trip_json_matches_openapi_shape() {
let created_at = Utc.with_ymd_and_hms(2026, 5, 25, 12, 0, 0).unwrap();
let mut status = SandboxStatus::new(
SandboxId::try_new("sandbox-abc123").unwrap(),
SandboxState::Running,
);
status.name = Some("fabro-01KSGHGMCFM8W2FHXNMJ7MVY65".to_string());
status.provider_state = "started".to_string();
let mut resources = Resources::default();
resources.cpu_cores = Some(2);
resources.memory_mb = Some(4096);
resources.disk_mb = Some(20 * 1024);
status.resources = Some(resources);
status.region = Some("us".to_string());
status
.labels
.insert("sh.fabro.managed".to_string(), "true".to_string());
status.snapshot = Some("daytona-medium".to_string());
status.network = Some(NetworkPolicy::Block);
status.web_url =
Some("https://app.daytona.io/dashboard/sandboxes?sandboxId=sandbox-abc123".to_string());
let at = SystemTime::from(DateTime::parse_from_rfc3339("2026-05-25T12:00:00Z").unwrap());
status.created_at = Some(at);
status.updated_at = Some(at);
let response = SandboxListResponse {
data: vec![SandboxInfo {
provider: SandboxProviderKind::DAYTONA,
id: "sandbox-abc123".to_string(),
display_name: Some("fabro-01KSGHGMCFM8W2FHXNMJ7MVY65".to_string()),
state: SandboxState::Running,
native_state: Some("started".to_string()),
image: None,
snapshot: Some("daytona-medium".to_string()),
region: Some("us".to_string()),
web_url: Some(
"https://app.daytona.io/dashboard/sandboxes?sandboxId=sandbox-abc123".to_string(),
),
working_directory: Some("/home/daytona/workspace".to_string()),
resources: SandboxResources {
cpu_cores: Some(2.0),
memory_bytes: Some(4 * 1024 * 1024 * 1024),
disk_bytes: Some(20 * 1024 * 1024 * 1024),
},
network: SandboxNetwork {
egress: SandboxNetworkPolicy::open(),
ingress: SandboxNetworkPolicy::blocked(),
},
labels: BTreeMap::from([(
"sh.fabro.managed".to_string(),
"true".to_string(),
)]),
timestamps: SandboxTimestamps {
created_at: Some(created_at),
last_activity_at: Some(created_at),
},
provider: SandboxProviderKind::DAYTONA,
status,
}],
meta: SandboxListMeta {
provider_errors: vec![SandboxProviderLookupError {
provider: SandboxProviderKind::DOCKER,
message: "Failed to connect to Docker daemon".to_string(),
message: "docker daemon unreachable".to_string(),
}],
},
};
let value = serde_json::to_value(&response).unwrap();
assert_eq!(
serde_json::to_value(&response).unwrap(),
value,
json!({
"data": [{
"provider": "daytona",
"id": "sandbox-abc123",
"display_name": "fabro-01KSGHGMCFM8W2FHXNMJ7MVY65",
"state": "running",
"native_state": "started",
"snapshot": "daytona-medium",
"region": "us",
"web_url": "https://app.daytona.io/dashboard/sandboxes?sandboxId=sandbox-abc123",
"working_directory": "/home/daytona/workspace",
"resources": {
"cpu_cores": 2.0,
"memory_bytes": 4_294_967_296_u64,
"disk_bytes": 21_474_836_480_u64
},
"network": {
"egress": {
"mode": "open",
"cidrs": []
"status": {
"id": "sandbox-abc123",
"name": "fabro-01KSGHGMCFM8W2FHXNMJ7MVY65",
"state": "running",
"provider_state": "started",
"error_reason": null,
"resources": {
"cpu_cores": 2,
"memory_mb": 4096,
"disk_mb": 20480,
"gpus": null
},
"ingress": {
"mode": "blocked",
"cidrs": []
}
},
"labels": {
"sh.fabro.managed": "true"
},
"timestamps": {
"sandbox_kind": null,
"region": "us",
"labels": { "sh.fabro.managed": "true" },
"image": null,
"snapshot": "daytona-medium",
"network": "block",
"workspace_ownership": null,
"web_url": "https://app.daytona.io/dashboard/sandboxes?sandboxId=sandbox-abc123",
"created_at": "2026-05-25T12:00:00Z",
"last_activity_at": "2026-05-25T12:00:00Z"
"updated_at": "2026-05-25T12:00:00Z"
}
}],
"meta": {
"provider_errors": [{
"provider": "docker",
"message": "Failed to connect to Docker daemon"
"message": "docker daemon unreachable"
}]
}
})
);
let decoded: ApiSandboxListResponse = serde_json::from_value(value).unwrap();
assert_eq!(decoded.data[0].provider, SandboxProviderKind::DAYTONA);
assert_eq!(decoded.data[0].status.state, SandboxState::Running);
assert!(matches!(
decoded.data[0].status.network,
Some(NetworkPolicy::Block)
));
}
fn assert_same_type<T: 'static, U: 'static>() {
fn assert_same_type<A: 'static, B: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
TypeId::of::<A>(),
TypeId::of::<B>(),
"{} should be {}",
type_name::<A>(),
type_name::<B>()
);
}

View file

@ -24,6 +24,7 @@ dirs.workspace = true
fabro-util = { path = "../fabro-util" }
hex.workspace = true
lithos-llm = { workspace = true, features = ["runtime"] }
sandbox-driver.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true

View file

@ -157,10 +157,7 @@ pub use run_summary::{
pub use run_title::{
MAX_RUN_TITLE_CHARS, RunTitleError, infer_run_title, normalize_explicit_run_title,
};
pub use sandbox_details::{
SandboxDetails, SandboxNetwork, SandboxNetworkPolicy, SandboxNetworkPolicyMode,
SandboxResources, SandboxState, SandboxTimestamps,
};
pub use sandbox_details::SandboxDetails;
pub use sandbox_inventory::{
SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxProviderLookupError,
};

View file

@ -1,197 +1,44 @@
use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::de::Error as _;
use sandbox_driver::SandboxStatus;
use serde::{Deserialize, Serialize};
use crate::RunSandboxInstance;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
/// The sandbox owned by a run: fabro's record of it, and the status the
/// sandbox driver reports for it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxDetails {
pub sandbox: RunSandboxInstance,
pub state: SandboxState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native_state: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web_url: Option<String>,
pub resources: SandboxResources,
#[serde(default)]
pub network: SandboxNetwork,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub labels: BTreeMap<String, String>,
pub timestamps: SandboxTimestamps,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SandboxState {
Unknown,
Provisioning,
Starting,
Running,
Stopping,
Stopped,
Paused,
Deleting,
Deleted,
Archived,
Restoring,
Resizing,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct SandboxResources {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_cores: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disk_bytes: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SandboxNetwork {
pub egress: SandboxNetworkPolicy,
pub ingress: SandboxNetworkPolicy,
}
impl SandboxNetwork {
pub fn unknown() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize)]
pub struct SandboxNetworkPolicy {
mode: SandboxNetworkPolicyMode,
cidrs: Vec<String>,
}
impl SandboxNetworkPolicy {
pub fn unknown() -> Self {
Self::default()
}
pub fn mode(&self) -> SandboxNetworkPolicyMode {
self.mode
}
pub fn cidrs(&self) -> &[String] {
&self.cidrs
}
pub fn open() -> Self {
Self {
mode: SandboxNetworkPolicyMode::Open,
cidrs: Vec::new(),
}
}
pub fn blocked() -> Self {
Self {
mode: SandboxNetworkPolicyMode::Blocked,
cidrs: Vec::new(),
}
}
pub fn allow_cidrs<I, S>(cidrs: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let cidrs: Vec<String> = cidrs.into_iter().map(Into::into).collect();
if cidrs.is_empty() {
return Self::unknown();
}
Self {
mode: SandboxNetworkPolicyMode::CidrAllowList,
cidrs,
}
}
pub fn essentials_only() -> Self {
Self {
mode: SandboxNetworkPolicyMode::EssentialsOnly,
cidrs: Vec::new(),
}
}
}
impl<'de> Deserialize<'de> for SandboxNetworkPolicy {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Wire {
#[serde(default)]
mode: SandboxNetworkPolicyMode,
#[serde(default)]
cidrs: Vec<String>,
}
let wire = Wire::deserialize(deserializer)?;
match wire.mode {
SandboxNetworkPolicyMode::CidrAllowList => {
if wire.cidrs.is_empty() {
return Err(D::Error::custom(
"cidr_allow_list network policy requires at least one CIDR",
));
}
Ok(Self::allow_cidrs(wire.cidrs))
}
mode => {
if !wire.cidrs.is_empty() {
return Err(D::Error::custom(
"network policy CIDRs are only valid for cidr_allow_list mode",
));
}
Ok(Self {
mode,
cidrs: Vec::new(),
})
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SandboxNetworkPolicyMode {
#[default]
Unknown,
Open,
Blocked,
CidrAllowList,
EssentialsOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct SandboxTimestamps {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_activity_at: Option<DateTime<Utc>>,
pub sandbox: RunSandboxInstance,
pub status: SandboxStatus,
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use std::time::SystemTime;
use chrono::DateTime;
use sandbox_driver::{SandboxId, SandboxState};
use serde_json::json;
use super::*;
use crate::{RunSandboxRuntime, SandboxProviderKind};
#[test]
fn serializes_with_snake_case_state() {
fn details_carry_the_record_and_the_drivers_status() {
let mut status = SandboxStatus::new(
SandboxId::try_new("container-abc123").unwrap(),
SandboxState::Running,
);
status.provider_state = "running".to_string();
status.image = Some("ghcr.io/fabro/sandbox:latest".to_string());
status.created_at = Some(SystemTime::from(
DateTime::parse_from_rfc3339("2026-05-09T12:00:00Z").unwrap(),
));
let details = SandboxDetails {
sandbox: RunSandboxInstance {
provider: crate::SandboxProviderKind::DOCKER,
sandbox: RunSandboxInstance {
provider: SandboxProviderKind::DOCKER,
image: Some("ghcr.io/fabro/sandbox:latest".to_string()),
snapshot: None,
runtime: crate::RunSandboxRuntime {
runtime: RunSandboxRuntime {
id: "container-abc123".to_string(),
working_directory: "/workspace".to_string(),
repo_cloned: None,
@ -203,168 +50,39 @@ mod tests {
primary_repo_link: None,
},
},
state: SandboxState::Running,
native_state: Some("running".to_string()),
region: None,
web_url: Some(
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9"
.to_string(),
),
resources: SandboxResources {
cpu_cores: Some(2.0),
memory_bytes: Some(4 * 1024 * 1024 * 1024),
disk_bytes: None,
},
network: SandboxNetwork {
egress: SandboxNetworkPolicy::allow_cidrs(["10.0.0.0/8"]),
ingress: SandboxNetworkPolicy::unknown(),
},
labels: BTreeMap::from([("run".to_string(), "abc".to_string())]),
timestamps: SandboxTimestamps {
created_at: Some(Utc.with_ymd_and_hms(2026, 5, 9, 12, 0, 0).unwrap()),
last_activity_at: None,
},
status,
};
assert_eq!(
serde_json::to_value(&details).unwrap(),
json!({
"sandbox": {
"provider": "docker",
"image": "ghcr.io/fabro/sandbox:latest",
"runtime": {
"id": "container-abc123",
"working_directory": "/workspace"
}
},
"state": "running",
"native_state": "running",
"web_url": "https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9",
"resources": {
"cpu_cores": 2.0,
"memory_bytes": 4_294_967_296_u64,
},
"network": {
"egress": {
"mode": "cidr_allow_list",
"cidrs": ["10.0.0.0/8"]
},
"ingress": {
"mode": "unknown",
"cidrs": []
}
},
"labels": {
"run": "abc"
},
"timestamps": {
"created_at": "2026-05-09T12:00:00Z"
}
})
);
let value = serde_json::to_value(&details).unwrap();
assert_eq!(value["sandbox"]["provider"], "docker");
assert_eq!(value["sandbox"]["runtime"]["id"], "container-abc123");
assert_eq!(value["status"]["id"], "container-abc123");
assert_eq!(value["status"]["state"], "running");
assert_eq!(value["status"]["image"], "ghcr.io/fabro/sandbox:latest");
assert_eq!(value["status"]["snapshot"], json!(null));
assert_eq!(value["status"]["created_at"], "2026-05-09T12:00:00Z");
let decoded: SandboxDetails = serde_json::from_value(value).unwrap();
assert_eq!(decoded.status.state, SandboxState::Running);
assert_eq!(decoded.status.provider_state, "running");
}
#[test]
fn deserializes_with_minimal_fields() {
fn a_status_with_only_its_required_fields_decodes() {
let details: SandboxDetails = serde_json::from_value(json!({
"sandbox": {
"provider": "local",
"image": null,
"snapshot": null,
"runtime": {
"id": "local:01JNQVR7M0EJ5GKAT2SC4ERS1Z",
"working_directory": "/Users/client/project"
}
"runtime": {
"id": "host-dir-2f746d70",
"working_directory": "/tmp"
}
},
"state": "unknown",
"resources": {},
"timestamps": {}
"status": { "id": "host-dir-2f746d70", "state": "running" }
}))
.unwrap();
assert_eq!(details.sandbox.provider, crate::SandboxProviderKind::LOCAL);
assert_eq!(
details.sandbox.runtime.id.as_str(),
"local:01JNQVR7M0EJ5GKAT2SC4ERS1Z"
);
assert_eq!(
details.sandbox.runtime.working_directory.as_str(),
"/Users/client/project"
);
assert_eq!(details.state, SandboxState::Unknown);
assert!(details.sandbox.image.is_none());
assert!(details.labels.is_empty());
assert_eq!(details.resources, SandboxResources::default());
assert_eq!(details.network, SandboxNetwork::unknown());
assert_eq!(details.timestamps, SandboxTimestamps::default());
}
#[test]
fn network_policy_helpers_cover_supported_modes() {
assert_eq!(
SandboxNetworkPolicy::unknown().mode(),
SandboxNetworkPolicyMode::Unknown
);
assert_eq!(
SandboxNetworkPolicy::open().mode(),
SandboxNetworkPolicyMode::Open
);
assert_eq!(
SandboxNetworkPolicy::blocked().mode(),
SandboxNetworkPolicyMode::Blocked
);
assert_eq!(
SandboxNetworkPolicy::allow_cidrs(["192.168.0.0/16", "10.0.0.0/8"]).cidrs(),
["192.168.0.0/16".to_string(), "10.0.0.0/8".to_string()]
);
assert_eq!(
SandboxNetworkPolicy::essentials_only().mode(),
SandboxNetworkPolicyMode::EssentialsOnly,
);
}
#[test]
fn network_policy_deserialization_rejects_empty_cidr_allow_list() {
assert!(
serde_json::from_value::<SandboxNetworkPolicy>(json!({
"mode": "cidr_allow_list",
"cidrs": []
}))
.is_err()
);
}
#[test]
fn network_policy_deserialization_rejects_cidrs_for_non_cidr_mode() {
assert!(
serde_json::from_value::<SandboxNetworkPolicy>(json!({
"mode": "open",
"cidrs": ["10.0.0.0/8"]
}))
.is_err()
);
}
#[test]
fn state_serializes_each_variant_in_snake_case() {
fn check(state: SandboxState, expected: &str) {
assert_eq!(
serde_json::to_value(state).unwrap(),
serde_json::Value::String(expected.to_string()),
);
}
check(SandboxState::Unknown, "unknown");
check(SandboxState::Provisioning, "provisioning");
check(SandboxState::Starting, "starting");
check(SandboxState::Running, "running");
check(SandboxState::Stopping, "stopping");
check(SandboxState::Stopped, "stopped");
check(SandboxState::Paused, "paused");
check(SandboxState::Deleting, "deleting");
check(SandboxState::Deleted, "deleted");
check(SandboxState::Archived, "archived");
check(SandboxState::Restoring, "restoring");
check(SandboxState::Resizing, "resizing");
check(SandboxState::Error, "error");
assert_eq!(details.sandbox.provider, SandboxProviderKind::LOCAL);
assert_eq!(details.status.id.as_str(), "host-dir-2f746d70");
assert!(details.status.labels.is_empty());
assert!(details.status.created_at.is_none());
}
}

View file

@ -1,36 +1,14 @@
use std::collections::BTreeMap;
use sandbox_driver::SandboxStatus;
use serde::{Deserialize, Serialize};
use crate::{
SandboxNetwork, SandboxProviderKind, SandboxResources, SandboxState, SandboxTimestamps,
};
use crate::SandboxProviderKind;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
/// One sandbox of fabro's inventory: the provider fabro connected it
/// through, and the status the sandbox driver reports for it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxInfo {
pub provider: SandboxProviderKind,
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
pub state: SandboxState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native_state: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option<String>,
pub resources: SandboxResources,
#[serde(default)]
pub network: SandboxNetwork,
#[serde(default)]
pub labels: BTreeMap<String, String>,
pub timestamps: SandboxTimestamps,
pub provider: SandboxProviderKind,
pub status: SandboxStatus,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -45,7 +23,7 @@ pub struct SandboxListMeta {
pub provider_errors: Vec<SandboxProviderLookupError>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxListResponse {
pub data: Vec<SandboxInfo>,
pub meta: SandboxListMeta,

View file

@ -1,45 +1,37 @@
use std::collections::BTreeMap;
use std::time::SystemTime;
use chrono::{TimeZone, Utc};
use chrono::DateTime;
use fabro_types::{
SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxNetwork, SandboxNetworkPolicy,
SandboxProviderKind, SandboxProviderLookupError, SandboxResources, SandboxState,
SandboxTimestamps,
SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxProviderKind,
SandboxProviderLookupError,
};
use sandbox_driver::{NetworkPolicy, Resources, SandboxId, SandboxState, SandboxStatus};
use serde_json::json;
#[test]
fn sandbox_inventory_serializes_provider_backed_shape() {
let created_at = Utc.with_ymd_and_hms(2026, 5, 25, 12, 0, 0).unwrap();
fn sandbox_inventory_serializes_the_provider_and_the_drivers_status() {
let mut status = SandboxStatus::new(
SandboxId::try_new("container-abc123").unwrap(),
SandboxState::Running,
);
status.name = Some("fabro-run-abc".to_string());
status.provider_state = "running".to_string();
status.image = Some("buildpack-deps:noble".to_string());
let mut resources = Resources::default();
resources.cpu_cores = Some(2);
resources.memory_mb = Some(4096);
status.resources = Some(resources);
status.network = Some(NetworkPolicy::AllowAll);
status
.labels
.insert("sh.fabro.managed".to_string(), "true".to_string());
status.created_at = Some(SystemTime::from(
DateTime::parse_from_rfc3339("2026-05-25T12:00:00Z").unwrap(),
));
let response = SandboxListResponse {
data: vec![SandboxInfo {
provider: SandboxProviderKind::DOCKER,
id: "container-abc123".to_string(),
display_name: Some("fabro-run-abc".to_string()),
state: SandboxState::Running,
native_state: Some("running".to_string()),
image: Some("buildpack-deps:noble".to_string()),
snapshot: None,
region: None,
web_url: None,
working_directory: Some("/workspace".to_string()),
resources: SandboxResources {
cpu_cores: Some(2.0),
memory_bytes: Some(4 * 1024 * 1024 * 1024),
disk_bytes: None,
},
network: SandboxNetwork {
egress: SandboxNetworkPolicy::open(),
ingress: SandboxNetworkPolicy::blocked(),
},
labels: BTreeMap::from([(
"sh.fabro.managed".to_string(),
"true".to_string(),
)]),
timestamps: SandboxTimestamps {
created_at: Some(created_at),
last_activity_at: None,
},
provider: SandboxProviderKind::DOCKER,
status,
}],
meta: SandboxListMeta {
provider_errors: vec![SandboxProviderLookupError {
@ -54,31 +46,28 @@ fn sandbox_inventory_serializes_provider_backed_shape() {
json!({
"data": [{
"provider": "docker",
"id": "container-abc123",
"display_name": "fabro-run-abc",
"state": "running",
"native_state": "running",
"image": "buildpack-deps:noble",
"working_directory": "/workspace",
"resources": {
"cpu_cores": 2.0,
"memory_bytes": 4_294_967_296_u64
},
"network": {
"egress": {
"mode": "open",
"cidrs": []
"status": {
"id": "container-abc123",
"name": "fabro-run-abc",
"state": "running",
"provider_state": "running",
"error_reason": null,
"resources": {
"cpu_cores": 2,
"memory_mb": 4096,
"disk_mb": null,
"gpus": null
},
"ingress": {
"mode": "blocked",
"cidrs": []
}
},
"labels": {
"sh.fabro.managed": "true"
},
"timestamps": {
"created_at": "2026-05-25T12:00:00Z"
"sandbox_kind": null,
"region": null,
"labels": { "sh.fabro.managed": "true" },
"image": "buildpack-deps:noble",
"snapshot": null,
"network": "allow_all",
"workspace_ownership": null,
"web_url": null,
"created_at": "2026-05-25T12:00:00Z",
"updated_at": null
}
}],
"meta": {
@ -92,28 +81,18 @@ fn sandbox_inventory_serializes_provider_backed_shape() {
}
#[test]
fn sandbox_inventory_deserializes_when_optional_fields_are_absent() {
fn sandbox_inventory_deserializes_a_status_with_only_its_required_fields() {
let info: SandboxInfo = serde_json::from_value(json!({
"provider": "local",
"id": "local:01KSGHGMCFM8W2FHXNMJ7MVY65",
"state": "unknown",
"resources": {},
"timestamps": {}
"status": { "id": "host-dir-2f746d70", "state": "unknown" }
}))
.unwrap();
assert_eq!(info.provider, SandboxProviderKind::LOCAL);
assert_eq!(info.id, "local:01KSGHGMCFM8W2FHXNMJ7MVY65");
assert_eq!(info.state, SandboxState::Unknown);
assert!(info.display_name.is_none());
assert!(info.native_state.is_none());
assert!(info.image.is_none());
assert!(info.snapshot.is_none());
assert!(info.region.is_none());
assert!(info.web_url.is_none());
assert!(info.working_directory.is_none());
assert_eq!(info.resources, SandboxResources::default());
assert_eq!(info.network, SandboxNetwork::unknown());
assert!(info.labels.is_empty());
assert_eq!(info.timestamps, SandboxTimestamps::default());
assert_eq!(info.status.id.as_str(), "host-dir-2f746d70");
assert_eq!(info.status.state, SandboxState::Unknown);
assert!(info.status.name.is_none());
assert!(info.status.resources.is_none());
assert!(info.status.network.is_none());
assert!(info.status.labels.is_empty());
}

View file

@ -1,10 +1,8 @@
use std::collections::BTreeMap;
use chrono::{TimeZone, Utc};
use fabro_types::{
RunSandbox, RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime, SandboxDetails,
SandboxNetwork, SandboxProviderKind, SandboxResources, SandboxState, SandboxTimestamps,
SandboxProviderKind,
};
use sandbox_driver::{SandboxId, SandboxState, SandboxStatus};
use serde_json::json;
#[test]
@ -72,9 +70,19 @@ fn run_sandbox_ready_requires_instance() {
}
#[test]
fn sandbox_details_requires_canonical_id_and_working_directory() {
fn sandbox_details_keep_the_record_beside_the_status() {
let mut status = SandboxStatus::new(
SandboxId::try_new("daytona-sandbox-name").unwrap(),
SandboxState::Running,
);
status.provider_state = "started".to_string();
status.region = Some("us".to_string());
status.web_url = Some(
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9"
.to_string(),
);
let details = SandboxDetails {
sandbox: RunSandboxInstance {
sandbox: RunSandboxInstance {
provider: SandboxProviderKind::DAYTONA,
image: Some("ubuntu:24.04".to_string()),
snapshot: None,
@ -90,24 +98,7 @@ fn sandbox_details_requires_canonical_id_and_working_directory() {
primary_repo_link: None,
},
},
state: SandboxState::Running,
native_state: Some("started".to_string()),
region: Some("us".to_string()),
web_url: Some(
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9"
.to_string(),
),
resources: SandboxResources {
cpu_cores: Some(2.0),
memory_bytes: Some(4 * 1024 * 1024 * 1024),
disk_bytes: None,
},
network: SandboxNetwork::unknown(),
labels: BTreeMap::from([("run".to_string(), "abc".to_string())]),
timestamps: SandboxTimestamps {
created_at: Some(Utc.with_ymd_and_hms(2026, 5, 9, 12, 0, 0).unwrap()),
last_activity_at: None,
},
status,
};
let value = serde_json::to_value(&details).unwrap();
@ -127,12 +118,11 @@ fn sandbox_details_requires_canonical_id_and_working_directory() {
"/home/daytona/repos"
);
assert_eq!(
value["web_url"],
value["status"]["web_url"],
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9"
);
assert_eq!(value["network"]["egress"]["mode"], "unknown");
assert_eq!(value["network"]["ingress"]["mode"], "unknown");
assert!(value.get("name").is_none());
assert_eq!(value["status"]["provider_state"], "started");
assert_eq!(value["status"]["network"], serde_json::Value::Null);
assert!(value.get("identifier").is_none());
}

View file

@ -432,11 +432,14 @@ models/sandbox-details.ts
models/sandbox-file-entry.ts
models/sandbox-file-list-response.ts
models/sandbox-info.ts
models/sandbox-kind.ts
models/sandbox-list-meta.ts
models/sandbox-list-response.ts
models/sandbox-network-policy-mode.ts
models/sandbox-network-policy-one-of-cidr-allow-list.ts
models/sandbox-network-policy-one-of.ts
models/sandbox-network-policy-one-of1-domain-allow-list.ts
models/sandbox-network-policy-one-of1.ts
models/sandbox-network-policy.ts
models/sandbox-network.ts
models/sandbox-plugin-settings.ts
models/sandbox-provider-lookup-error.ts
models/sandbox-resources.ts
@ -445,7 +448,8 @@ models/sandbox-service-list-meta.ts
models/sandbox-service-list-response.ts
models/sandbox-service.ts
models/sandbox-state.ts
models/sandbox-timestamps.ts
models/sandbox-status.ts
models/sandbox-workspace-ownership.ts
models/save-query-request.ts
models/saved-query.ts
models/secret-list-response.ts

View file

@ -656,7 +656,7 @@ export const HumanInTheLoopApiAxiosParamCreator = function (configuration?: Conf
};
},
/**
* Returns provider-neutral details about the sandbox owned by this run, including identity, normalized state, image/snapshot, resources, labels, and timestamps.
* Returns the sandbox owned by this run as fabro\'s record of it plus the sandbox driver\'s status (identity, state, image or snapshot, resources, network policy, labels, and timestamps).
* @summary Retrieve Run Sandbox Details
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
@ -1076,7 +1076,7 @@ export const HumanInTheLoopApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Returns provider-neutral details about the sandbox owned by this run, including identity, normalized state, image/snapshot, resources, labels, and timestamps.
* Returns the sandbox owned by this run as fabro\'s record of it plus the sandbox driver\'s status (identity, state, image or snapshot, resources, network policy, labels, and timestamps).
* @summary Retrieve Run Sandbox Details
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
@ -1300,7 +1300,7 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration,
return localVarFp.putSandboxFile(id, path, body, options).then((request) => request(axios, basePath));
},
/**
* Returns provider-neutral details about the sandbox owned by this run, including identity, normalized state, image/snapshot, resources, labels, and timestamps.
* Returns the sandbox owned by this run as fabro\'s record of it plus the sandbox driver\'s status (identity, state, image or snapshot, resources, network policy, labels, and timestamps).
* @summary Retrieve Run Sandbox Details
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
@ -1520,7 +1520,7 @@ export class HumanInTheLoopApi extends BaseAPI {
}
/**
* Returns provider-neutral details about the sandbox owned by this run, including identity, normalized state, image/snapshot, resources, labels, and timestamps.
* Returns the sandbox owned by this run as fabro\'s record of it plus the sandbox driver\'s status (identity, state, image or snapshot, resources, network policy, labels, and timestamps).
* @summary Retrieve Run Sandbox Details
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.

View file

@ -402,11 +402,14 @@ export * from './sandbox-details';
export * from './sandbox-file-entry';
export * from './sandbox-file-list-response';
export * from './sandbox-info';
export * from './sandbox-kind';
export * from './sandbox-list-meta';
export * from './sandbox-list-response';
export * from './sandbox-network';
export * from './sandbox-network-policy';
export * from './sandbox-network-policy-mode';
export * from './sandbox-network-policy-one-of';
export * from './sandbox-network-policy-one-of1';
export * from './sandbox-network-policy-one-of1-domain-allow-list';
export * from './sandbox-network-policy-one-of-cidr-allow-list';
export * from './sandbox-plugin-settings';
export * from './sandbox-provider-lookup-error';
export * from './sandbox-resources';
@ -415,7 +418,8 @@ export * from './sandbox-service-discovery-source';
export * from './sandbox-service-list-meta';
export * from './sandbox-service-list-response';
export * from './sandbox-state';
export * from './sandbox-timestamps';
export * from './sandbox-status';
export * from './sandbox-workspace-ownership';
export * from './save-query-request';
export * from './saved-query';
export * from './secret-list-response';

View file

@ -17,7 +17,7 @@
export interface RunCheckpointSettings {
'exclude_globs': Array<string>;
/**
* When true, Fabro-managed run-branch checkpoint commits bypass local Git commit hooks. Does not affect Fabro `[[run.hooks]]` or metadata-branch snapshots. Defaults to false.
* Accepted for compatibility. Fabro-managed run-branch checkpoint commits never run local Git commit hooks: the sandbox driver disables repository hooks on every git command it runs. Does not affect Fabro `[[run.hooks]]`. Defaults to false.
*/
'skip_git_hooks': boolean;
}

View file

@ -18,40 +18,12 @@
import type { RunSandboxInstance } from './run-sandbox-instance';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetwork } from './sandbox-network';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxResources } from './sandbox-resources';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxState } from './sandbox-state';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxTimestamps } from './sandbox-timestamps';
import type { SandboxStatus } from './sandbox-status';
/**
* Provider-neutral details about the sandbox owned by a run.
* The sandbox owned by a run, as fabro\'s record of it and the sandbox driver\'s status.
*/
export interface SandboxDetails {
'sandbox': RunSandboxInstance;
'state': SandboxState;
/**
* Original provider state string before normalization. Display/debugging only; UI behavior keys off `state`.
*/
'native_state'?: string | null;
/**
* Provider region or target. Null for local-style providers.
*/
'region'?: string | null;
/**
* Provider dashboard URL for this sandbox when available.
*/
'web_url'?: string | null;
'resources': SandboxResources;
'network': SandboxNetwork;
/**
* Provider-reported labels.
*/
'labels': { [key: string]: string; };
'timestamps': SandboxTimestamps;
'status': SandboxStatus;
}

View file

@ -15,63 +15,15 @@
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetwork } from './sandbox-network';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxResources } from './sandbox-resources';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxState } from './sandbox-state';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxTimestamps } from './sandbox-timestamps';
import type { SandboxStatus } from './sandbox-status';
/**
* Provider-backed inventory record for a Fabro-managed sandbox.
* One sandbox of fabro\'s provider-backed inventory, as the provider fabro connected it through and the sandbox driver\'s status.
*/
export interface SandboxInfo {
/**
* Sandbox provider kind. `local`, `docker`, and `daytona` are bundled with the server; any other value names a sandbox-driver plugin configured under `server.sandbox.providers.<kind>`.
*/
'provider': string;
/**
* Provider-native sandbox id.
*/
'id': string;
/**
* Provider display name when distinct from the native id.
*/
'display_name'?: string | null;
'state': SandboxState;
/**
* Original provider state string before normalization. Display/debugging only; UI behavior keys off `state`.
*/
'native_state'?: string | null;
/**
* Provider image when surfaced by the sandbox provider.
*/
'image'?: string | null;
/**
* Provider snapshot when surfaced by the sandbox provider.
*/
'snapshot'?: string | null;
/**
* Provider region or target. Null for local-style providers.
*/
'region'?: string | null;
/**
* Provider dashboard URL for this sandbox when available.
*/
'web_url'?: string | null;
/**
* Provider-reported or Fabro-default working directory when available.
*/
'working_directory'?: string | null;
'resources': SandboxResources;
'network': SandboxNetwork;
/**
* Provider-reported labels.
*/
'labels': { [key: string]: string; };
'timestamps': SandboxTimestamps;
'status': SandboxStatus;
}

View file

@ -15,15 +15,13 @@
/**
* Lifecycle timestamps for a sandbox. Fields are nullable when the provider does not surface a value.
* The kind of isolation a sandbox was provisioned with, as observed by the driver. Not an isolation guarantee.
*/
export interface SandboxTimestamps {
/**
* When the sandbox was created.
*/
'created_at'?: string;
/**
* Most recent activity timestamp reported by the provider.
*/
'last_activity_at'?: string;
}
export const SandboxKind = {
CONTAINER: 'container',
VIRTUAL_MACHINE: 'virtual_machine',
UNKNOWN: 'unknown'
} as const;
export type SandboxKind = typeof SandboxKind[keyof typeof SandboxKind];

View file

@ -1,29 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Provider-neutral public-network policy for one direction.
*/
export const SandboxNetworkPolicyMode = {
UNKNOWN: 'unknown',
OPEN: 'open',
BLOCKED: 'blocked',
CIDR_ALLOW_LIST: 'cidr_allow_list',
ESSENTIALS_ONLY: 'essentials_only'
} as const;
export type SandboxNetworkPolicyMode = typeof SandboxNetworkPolicyMode[keyof typeof SandboxNetworkPolicyMode];

View file

@ -0,0 +1,19 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface SandboxNetworkPolicyOneOfCidrAllowList {
'cidrs': Array<string>;
}

View file

@ -15,12 +15,8 @@
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetworkPolicy } from './sandbox-network-policy';
import type { SandboxNetworkPolicyOneOfCidrAllowList } from './sandbox-network-policy-one-of-cidr-allow-list';
/**
* Provider-neutral public-network policy for sandbox egress and ingress.
*/
export interface SandboxNetwork {
'egress': SandboxNetworkPolicy;
'ingress': SandboxNetworkPolicy;
export interface SandboxNetworkPolicyOneOf {
'cidr_allow_list': SandboxNetworkPolicyOneOfCidrAllowList;
}

View file

@ -0,0 +1,19 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface SandboxNetworkPolicyOneOf1DomainAllowList {
'domains': Array<string>;
}

View file

@ -0,0 +1,22 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetworkPolicyOneOf1DomainAllowList } from './sandbox-network-policy-one-of1-domain-allow-list';
export interface SandboxNetworkPolicyOneOf1 {
'domain_allow_list': SandboxNetworkPolicyOneOf1DomainAllowList;
}

View file

@ -15,15 +15,19 @@
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetworkPolicyMode } from './sandbox-network-policy-mode';
import type { SandboxNetworkPolicyOneOf } from './sandbox-network-policy-one-of';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetworkPolicyOneOf1 } from './sandbox-network-policy-one-of1';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetworkPolicyOneOf1DomainAllowList } from './sandbox-network-policy-one-of1-domain-allow-list';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetworkPolicyOneOfCidrAllowList } from './sandbox-network-policy-one-of-cidr-allow-list';
/**
* Public-network policy for one direction.
* @type SandboxNetworkPolicy
* The network policy in force for a sandbox. A policy without parameters is its name; an allow list carries its entries.
*/
export interface SandboxNetworkPolicy {
'mode': SandboxNetworkPolicyMode;
/**
* CIDR entries when `mode` is `cidr_allow_list`; empty for other modes.
*/
'cidrs': Array<string>;
}
export type SandboxNetworkPolicy = SandboxNetworkPolicyOneOf | SandboxNetworkPolicyOneOf1 | string;

View file

@ -15,19 +15,11 @@
/**
* Resource configuration for a sandbox. Fields are nullable when the provider does not surface a value or no limit is configured.
* Compute resources of a sandbox, in the units the field names give. A field is null when the provider does not report a value or applies its default.
*/
export interface SandboxResources {
/**
* Configured CPU cores. Null when unavailable.
*/
'cpu_cores'?: number;
/**
* Memory limit in bytes. Null when unavailable or unlimited.
*/
'memory_bytes'?: number;
/**
* Disk size in bytes. Null when unavailable.
*/
'disk_bytes'?: number;
'cpu_cores'?: number | null;
'memory_mb'?: number | null;
'disk_mb'?: number | null;
'gpus'?: number | null;
}

View file

@ -15,23 +15,28 @@
/**
* Normalized sandbox lifecycle state used by the control plane and UI. The original provider-specific state string is preserved in `native_state`.
* The sandbox driver\'s lifecycle state for a sandbox. The provider\'s own state string is preserved in `SandboxStatus.provider_state`. A reader must treat a value it does not know as `unknown`.
*/
export const SandboxState = {
UNKNOWN: 'unknown',
PROVISIONING: 'provisioning',
CREATING: 'creating',
STARTING: 'starting',
RUNNING: 'running',
STOPPING: 'stopping',
STOPPED: 'stopped',
PAUSING: 'pausing',
PAUSED: 'paused',
DELETING: 'deleting',
DELETED: 'deleted',
RESUMING: 'resuming',
ARCHIVING: 'archiving',
ARCHIVED: 'archived',
RESTORING: 'restoring',
RESIZING: 'resizing',
ERROR: 'error'
FORKING: 'forking',
SNAPSHOTTING: 'snapshotting',
DELETING: 'deleting',
DELETED: 'deleted',
ERROR: 'error',
UNKNOWN: 'unknown'
} as const;
export type SandboxState = typeof SandboxState[keyof typeof SandboxState];

View file

@ -0,0 +1,79 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxKind } from './sandbox-kind';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxNetworkPolicy } from './sandbox-network-policy';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxResources } from './sandbox-resources';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxState } from './sandbox-state';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxWorkspaceOwnership } from './sandbox-workspace-ownership';
/**
* What the sandbox driver reports about a sandbox. Only `id` and `state` are always present; every other field is null or empty when the provider does not report it.
*/
export interface SandboxStatus {
/**
* The provider\'s stable identifier for the sandbox.
*/
'id': string;
/**
* The provider\'s display name, which is not the stable identifier.
*/
'name'?: string | null;
'state': SandboxState;
/**
* The provider\'s own state string, for display and debugging.
*/
'provider_state'?: string;
'error_reason'?: string | null;
'resources'?: SandboxResources | null;
'sandbox_kind'?: SandboxKind | null;
/**
* The provider region or target the sandbox runs in.
*/
'region'?: string | null;
/**
* Provider-stored labels, including fabro\'s ownership labels.
*/
'labels'?: { [key: string]: string; };
/**
* The image the sandbox runs, when the provider knows it (a Docker container\'s image reference).
*/
'image'?: string | null;
/**
* The snapshot the sandbox was created from, when the provider knows it (a Daytona snapshot name).
*/
'snapshot'?: string | null;
'network'?: SandboxNetworkPolicy | null;
'workspace_ownership'?: SandboxWorkspaceOwnership | null;
/**
* The provider\'s console page for the sandbox, when it has one.
*/
'web_url'?: string | null;
'created_at'?: string | null;
/**
* The provider\'s most recent activity or update timestamp for the sandbox.
*/
'updated_at'?: string | null;
}

View file

@ -0,0 +1,26 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Who owns a local sandbox\'s workspace directory. `designated` is a caller-owned directory that deleting the sandbox never touches; `managed` is a directory the driver created and removes.
*/
export const SandboxWorkspaceOwnership = {
DESIGNATED: 'designated',
MANAGED: 'managed'
} as const;
export type SandboxWorkspaceOwnership = typeof SandboxWorkspaceOwnership[keyof typeof SandboxWorkspaceOwnership];