From d24042ba6472e7ceb1d7d30423438ff49569cf6b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 11 Sep 2026 13:07:00 -0600 Subject: [PATCH] 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 --- Cargo.lock | 2 + .../app/components/run-summary-panel.test.tsx | 2 +- .../app/components/run-summary-panel.tsx | 10 +- apps/fabro-web/app/lib/sandbox-state.ts | 88 ++-- .../fabro-web/app/routes/run-sandbox.test.tsx | 128 +++--- apps/fabro-web/app/routes/run-sandbox.tsx | 109 ++--- docs/public/api-reference/fabro-api.yaml | 261 ++++++------ .../src/server/handler/sandbox.rs | 46 ++- .../src/server/handler/sandboxes.rs | 6 +- lib/components/fabro-sandbox/src/details.rs | 196 +-------- lib/components/fabro-sandbox/src/provider.rs | 18 +- lib/foundation/fabro-api/Cargo.toml | 1 + lib/foundation/fabro-api/build.rs | 18 +- lib/foundation/fabro-api/src/lib.rs | 30 +- .../tests/sandbox_details_round_trip.rs | 169 ++++---- .../tests/sandbox_inventory_round_trip.rs | 138 ++++--- lib/foundation/fabro-types/Cargo.toml | 1 + lib/foundation/fabro-types/src/lib.rs | 5 +- .../fabro-types/src/sandbox_details.rs | 376 +++--------------- .../fabro-types/src/sandbox_inventory.rs | 38 +- .../tests/sandbox_inventory_serde.rs | 131 +++--- .../fabro-types/tests/sandbox_model_serde.rs | 46 +-- .../src/.openapi-generator/FILES | 10 +- .../src/api/human-in-the-loop-api.ts | 8 +- .../fabro-api-client/src/models/index.ts | 10 +- .../src/models/run-checkpoint-settings.ts | 2 +- .../src/models/sandbox-details.ts | 34 +- .../src/models/sandbox-info.ts | 54 +-- ...{sandbox-timestamps.ts => sandbox-kind.ts} | 20 +- .../src/models/sandbox-network-policy-mode.ts | 29 -- ...x-network-policy-one-of-cidr-allow-list.ts | 19 + ...rk.ts => sandbox-network-policy-one-of.ts} | 10 +- ...etwork-policy-one-of1-domain-allow-list.ts | 19 + .../models/sandbox-network-policy-one-of1.ts | 22 + .../src/models/sandbox-network-policy.ts | 22 +- .../src/models/sandbox-resources.ts | 18 +- .../src/models/sandbox-state.ts | 17 +- .../src/models/sandbox-status.ts | 79 ++++ .../src/models/sandbox-workspace-ownership.ts | 26 ++ 39 files changed, 907 insertions(+), 1311 deletions(-) rename lib/packages/fabro-api-client/src/models/{sandbox-timestamps.ts => sandbox-kind.ts} (51%) delete mode 100644 lib/packages/fabro-api-client/src/models/sandbox-network-policy-mode.ts create mode 100644 lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of-cidr-allow-list.ts rename lib/packages/fabro-api-client/src/models/{sandbox-network.ts => sandbox-network-policy-one-of.ts} (60%) create mode 100644 lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of1-domain-allow-list.ts create mode 100644 lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of1.ts create mode 100644 lib/packages/fabro-api-client/src/models/sandbox-status.ts create mode 100644 lib/packages/fabro-api-client/src/models/sandbox-workspace-ownership.ts diff --git a/Cargo.lock b/Cargo.lock index 1f8141636..5c95dae90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/apps/fabro-web/app/components/run-summary-panel.test.tsx b/apps/fabro-web/app/components/run-summary-panel.test.tsx index 018184802..5ed6eca29 100644 --- a/apps/fabro-web/app/components/run-summary-panel.test.tsx +++ b/apps/fabro-web/app/components/run-summary-panel.test.tsx @@ -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"); }); diff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx index c3784a9ba..b462402a3 100644 --- a/apps/fabro-web/app/components/run-summary-panel.tsx +++ b/apps/fabro-web/app/components/run-summary-panel.tsx @@ -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 }) { = { 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 = 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, }, }; diff --git a/apps/fabro-web/app/routes/run-sandbox.test.tsx b/apps/fabro-web/app/routes/run-sandbox.test.tsx index 958d0a217..8772f9283 100644 --- a/apps/fabro-web/app/routes/run-sandbox.test.tsx +++ b/apps/fabro-web/app/routes/run-sandbox.test.tsx @@ -103,14 +103,15 @@ mock.restore(); const mountedRenderers: TestRenderer.ReactTestRenderer[] = []; function sandboxDetails( - overrides: Partial & { + overrides: { sandbox?: Partial & { runtime?: Partial>; }; + status?: Partial; } = {}, ): 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"] { - 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", () => { diff --git a/apps/fabro-web/app/routes/run-sandbox.tsx b/apps/fabro-web/app/routes/run-sandbox.tsx index d190bd5e0..c93a3f139 100644 --- a/apps/fabro-web/app/routes/run-sandbox.tsx +++ b/apps/fabro-web/app/routes/run-sandbox.tsx @@ -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 = { - unknown: "Unknown", - open: "Open", - blocked: "Blocked", - cidr_allow_list: "CIDR allow list", - essentials_only: "Essentials only", +const NETWORK_POLICY_DISPLAY: Record = { + 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 (
@@ -158,7 +179,7 @@ function StatusStrip({ details }: { details: SandboxDetails }) { {showNative && ( - ({details.native_state}) + ({providerState}) )}
@@ -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 ( - + - - {details.web_url && ( + + {status.sandbox_kind && } + {status.web_url && ( - - - + + + + {resources?.gpus != null && } ); } -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 ( - - - {cidrRows.map(({ label, policy }) => ( - - ))} + + {entries && ( + + )} ); } @@ -237,11 +259,8 @@ function LabelsPanel({ labels }: { labels: { [key: string]: string } | null | un function TimestampsPanel({ details }: { details: SandboxDetails }) { return ( - - + + ); } @@ -259,9 +278,9 @@ function DetailsColumn({ details }: { details: SandboxDetails | null }) {
- - - + + +
); diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index bd6e1cc7f..cef7dc3be 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -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. diff --git a/lib/apps/fabro-server/src/server/handler/sandbox.rs b/lib/apps/fabro-server/src/server/handler/sandbox.rs index e4601f33c..af65b4f71 100644 --- a/lib/apps/fabro-server/src/server/handler/sandbox.rs +++ b/lib/apps/fabro-server/src/server/handler/sandbox.rs @@ -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"))) diff --git a/lib/apps/fabro-server/src/server/handler/sandboxes.rs b/lib/apps/fabro-server/src/server/handler/sandboxes.rs index ddd1a7c88..d8b92cde2 100644 --- a/lib/apps/fabro-server/src/server/handler/sandboxes.rs +++ b/lib/apps/fabro-server/src/server/handler/sandboxes.rs @@ -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"); } diff --git a/lib/components/fabro-sandbox/src/details.rs b/lib/components/fabro-sandbox/src/details.rs index 3505028cc..28c0a40e4 100644 --- a/lib/components/fabro-sandbox/src/details.rs +++ b/lib/components/fabro-sandbox/src/details.rs @@ -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, - 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::::from), - last_activity_at: status.updated_at.map(DateTime::::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, + }) } diff --git a/lib/components/fabro-sandbox/src/provider.rs b/lib/components/fabro-sandbox/src/provider.rs index 6991fd34f..9ad32d28e 100644 --- a/lib/components/fabro-sandbox/src/provider.rs +++ b/lib/components/fabro-sandbox/src/provider.rs @@ -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); } diff --git a/lib/foundation/fabro-api/Cargo.toml b/lib/foundation/fabro-api/Cargo.toml index 02f85c3df..ec0ef3535 100644 --- a/lib/foundation/fabro-api/Cargo.toml +++ b/lib/foundation/fabro-api/Cargo.toml @@ -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"] } diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 71221d8ea..a05845c2b 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -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", &[]), diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index 794a934db..272913700 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -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::*; } diff --git a/lib/foundation/fabro-api/tests/sandbox_details_round_trip.rs b/lib/foundation/fabro-api/tests/sandbox_details_round_trip.rs index af742232b..bdfd35aae 100644 --- a/lib/foundation/fabro-api/tests/sandbox_details_round_trip.rs +++ b/lib/foundation/fabro-api/tests/sandbox_details_round_trip.rs @@ -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::(); assert_same_type::(); + assert_same_type::(); + assert_same_type::(); assert_same_type::(); - assert_same_type::(); - assert_same_type::(); - assert_same_type::(); - assert_same_type::(); - assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); } #[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() { +fn assert_same_type() { assert_eq!( - TypeId::of::(), - TypeId::of::(), - "{} should be the same type as {}", - type_name::(), - type_name::() + TypeId::of::(), + TypeId::of::(), + "{} should be {}", + type_name::(), + type_name::() ); } diff --git a/lib/foundation/fabro-api/tests/sandbox_inventory_round_trip.rs b/lib/foundation/fabro-api/tests/sandbox_inventory_round_trip.rs index 336c2f1e8..a802d8b15 100644 --- a/lib/foundation/fabro-api/tests/sandbox_inventory_round_trip.rs +++ b/lib/foundation/fabro-api/tests/sandbox_inventory_round_trip.rs @@ -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() { +fn assert_same_type() { assert_eq!( - TypeId::of::(), - TypeId::of::(), - "{} should be the same type as {}", - type_name::(), - type_name::() + TypeId::of::(), + TypeId::of::(), + "{} should be {}", + type_name::(), + type_name::() ); } diff --git a/lib/foundation/fabro-types/Cargo.toml b/lib/foundation/fabro-types/Cargo.toml index 4cd59e7bb..936676f43 100644 --- a/lib/foundation/fabro-types/Cargo.toml +++ b/lib/foundation/fabro-types/Cargo.toml @@ -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 diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index 829210788..0926cbdfd 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -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, }; diff --git a/lib/foundation/fabro-types/src/sandbox_details.rs b/lib/foundation/fabro-types/src/sandbox_details.rs index 7a33ba6ba..362b88168 100644 --- a/lib/foundation/fabro-types/src/sandbox_details.rs +++ b/lib/foundation/fabro-types/src/sandbox_details.rs @@ -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, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub web_url: Option, - pub resources: SandboxResources, - #[serde(default)] - pub network: SandboxNetwork, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub labels: BTreeMap, - 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, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub memory_bytes: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub disk_bytes: Option, -} - -#[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, -} - -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(cidrs: I) -> Self - where - I: IntoIterator, - S: Into, - { - let cidrs: Vec = 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(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Wire { - #[serde(default)] - mode: SandboxNetworkPolicyMode, - #[serde(default)] - cidrs: Vec, - } - - 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>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub last_activity_at: Option>, + 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::(json!({ - "mode": "cidr_allow_list", - "cidrs": [] - })) - .is_err() - ); - } - - #[test] - fn network_policy_deserialization_rejects_cidrs_for_non_cidr_mode() { - assert!( - serde_json::from_value::(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()); } } diff --git a/lib/foundation/fabro-types/src/sandbox_inventory.rs b/lib/foundation/fabro-types/src/sandbox_inventory.rs index 3acef2a78..c22c830e2 100644 --- a/lib/foundation/fabro-types/src/sandbox_inventory.rs +++ b/lib/foundation/fabro-types/src/sandbox_inventory.rs @@ -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, - pub state: SandboxState, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub native_state: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub image: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub snapshot: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub web_url: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, - pub resources: SandboxResources, - #[serde(default)] - pub network: SandboxNetwork, - #[serde(default)] - pub labels: BTreeMap, - 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, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SandboxListResponse { pub data: Vec, pub meta: SandboxListMeta, diff --git a/lib/foundation/fabro-types/tests/sandbox_inventory_serde.rs b/lib/foundation/fabro-types/tests/sandbox_inventory_serde.rs index dc733679d..2dbe0ab23 100644 --- a/lib/foundation/fabro-types/tests/sandbox_inventory_serde.rs +++ b/lib/foundation/fabro-types/tests/sandbox_inventory_serde.rs @@ -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()); } diff --git a/lib/foundation/fabro-types/tests/sandbox_model_serde.rs b/lib/foundation/fabro-types/tests/sandbox_model_serde.rs index ac10edc13..46648a17e 100644 --- a/lib/foundation/fabro-types/tests/sandbox_model_serde.rs +++ b/lib/foundation/fabro-types/tests/sandbox_model_serde.rs @@ -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()); } diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 87e029dc2..664fc9e7e 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -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 diff --git a/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts b/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts index 32b365ac2..6ad536c84 100644 --- a/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts +++ b/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts @@ -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. diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 88d3bc442..c407a60b6 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -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'; diff --git a/lib/packages/fabro-api-client/src/models/run-checkpoint-settings.ts b/lib/packages/fabro-api-client/src/models/run-checkpoint-settings.ts index 2342ac774..78ee1b5af 100644 --- a/lib/packages/fabro-api-client/src/models/run-checkpoint-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-checkpoint-settings.ts @@ -17,7 +17,7 @@ export interface RunCheckpointSettings { 'exclude_globs': Array; /** - * 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; } diff --git a/lib/packages/fabro-api-client/src/models/sandbox-details.ts b/lib/packages/fabro-api-client/src/models/sandbox-details.ts index 63075bcc9..d7e52d9ee 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-details.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-details.ts @@ -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; } diff --git a/lib/packages/fabro-api-client/src/models/sandbox-info.ts b/lib/packages/fabro-api-client/src/models/sandbox-info.ts index 571825e7e..1ef579a63 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-info.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-info.ts @@ -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.`. */ '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; } diff --git a/lib/packages/fabro-api-client/src/models/sandbox-timestamps.ts b/lib/packages/fabro-api-client/src/models/sandbox-kind.ts similarity index 51% rename from lib/packages/fabro-api-client/src/models/sandbox-timestamps.ts rename to lib/packages/fabro-api-client/src/models/sandbox-kind.ts index da431e4cb..eafb2cf53 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-timestamps.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-kind.ts @@ -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]; diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network-policy-mode.ts b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-mode.ts deleted file mode 100644 index 3a208c284..000000000 --- a/lib/packages/fabro-api-client/src/models/sandbox-network-policy-mode.ts +++ /dev/null @@ -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]; diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of-cidr-allow-list.ts b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of-cidr-allow-list.ts new file mode 100644 index 000000000..199d64256 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of-cidr-allow-list.ts @@ -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; +} diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network.ts b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of.ts similarity index 60% rename from lib/packages/fabro-api-client/src/models/sandbox-network.ts rename to lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of.ts index e378225e2..9e4e0b9f5 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-network.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of.ts @@ -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; } diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of1-domain-allow-list.ts b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of1-domain-allow-list.ts new file mode 100644 index 000000000..afd6f7c9b --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of1-domain-allow-list.ts @@ -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; +} diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of1.ts b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of1.ts new file mode 100644 index 000000000..2ec306f34 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-one-of1.ts @@ -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; +} diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network-policy.ts b/lib/packages/fabro-api-client/src/models/sandbox-network-policy.ts index b26016a62..4e3fc59a4 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-network-policy.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-network-policy.ts @@ -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; -} +export type SandboxNetworkPolicy = SandboxNetworkPolicyOneOf | SandboxNetworkPolicyOneOf1 | string; diff --git a/lib/packages/fabro-api-client/src/models/sandbox-resources.ts b/lib/packages/fabro-api-client/src/models/sandbox-resources.ts index f23c87f44..dd6b5ca24 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-resources.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-resources.ts @@ -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; } diff --git a/lib/packages/fabro-api-client/src/models/sandbox-state.ts b/lib/packages/fabro-api-client/src/models/sandbox-state.ts index c40419d2f..6e683f5db 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-state.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-state.ts @@ -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]; diff --git a/lib/packages/fabro-api-client/src/models/sandbox-status.ts b/lib/packages/fabro-api-client/src/models/sandbox-status.ts new file mode 100644 index 000000000..defff9a80 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-status.ts @@ -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; +} diff --git a/lib/packages/fabro-api-client/src/models/sandbox-workspace-ownership.ts b/lib/packages/fabro-api-client/src/models/sandbox-workspace-ownership.ts new file mode 100644 index 000000000..ad5814a45 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-workspace-ownership.ts @@ -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];