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];