fix(server): improve sandbox service discovery

Fall back to procfs when ss is unavailable, report the discovery source in API metadata, and surface the sandbox install tip in the services UI. Previewable services are ordered first for clearer service selection.
This commit is contained in:
Bryan Helmkamp 2026-05-10 12:40:38 -04:00
parent a74ae2e7f8
commit 47f581cc1d
No known key found for this signature in database
15 changed files with 432 additions and 69 deletions

View file

@ -25,6 +25,13 @@ function makeIdlePreview(): PreviewMutationShape {
};
}
function makeServicesData(data: SandboxService[]) {
return {
data,
meta: { source: "ss" as const },
};
}
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
interface WindowLike {
@ -97,7 +104,7 @@ describe("ServicesPanelView", () => {
test("shows empty state when the services list is empty", () => {
const renderer = renderView({
servicesQuery: { ...makeIdleQuery(), data: { data: [] } },
servicesQuery: { ...makeIdleQuery(), data: makeServicesData([]) },
previewMutation: makeIdlePreview(),
});
const titles = renderer.root.findAll(
@ -109,6 +116,46 @@ describe("ServicesPanelView", () => {
expect(titles).toHaveLength(1);
});
test("shows an iproute2 tip when services were discovered from procfs", () => {
const service: SandboxService = {
port: 3000,
addresses: ["0.0.0.0:3000"],
processes: [],
preview_supported: true,
};
const renderer = renderView({
servicesQuery: {
...makeIdleQuery(),
data: {
data: [service],
meta: { source: "procfs" },
},
},
previewMutation: makeIdlePreview(),
});
const tipLabels = renderer.root.findAll(
(node) =>
node.type === "span" &&
Array.isArray(node.children) &&
node.children.includes("Tip:"),
);
expect(tipLabels).toHaveLength(1);
const commands = renderer.root.findAll(
(node) =>
node.type === "code" &&
Array.isArray(node.children) &&
node.children.includes("apt-get install iproute2"),
);
expect(commands).toHaveLength(1);
const tipText = JSON.stringify(renderer.toJSON());
expect(tipText).toContain("Install ");
expect(tipText).toContain("ss");
expect(tipText).toContain(" in the sandbox for improved services listing:");
});
test("shows API error state with the error message", () => {
const renderer = renderView({
servicesQuery: {
@ -139,7 +186,7 @@ describe("ServicesPanelView", () => {
preview_supported: false,
};
const renderer = renderView({
servicesQuery: { ...makeIdleQuery(), data: { data: [service] } },
servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) },
previewMutation: makeIdlePreview(),
});
@ -158,14 +205,6 @@ describe("ServicesPanelView", () => {
node.children.includes("Preview"),
);
expect(previewButtons).toHaveLength(0);
const unavailable = renderer.root.findAll(
(node) =>
node.type === "span" &&
Array.isArray(node.children) &&
node.children.includes("Unavailable"),
);
expect(unavailable).toHaveLength(1);
});
test("renders a Preview button for a previewable service and triggers with signed args", async () => {
@ -186,7 +225,7 @@ describe("ServicesPanelView", () => {
installWindowOpen(windowOpenSpy);
const renderer = renderView({
servicesQuery: { ...makeIdleQuery(), data: { data: [service] } },
servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) },
previewMutation: { trigger },
});
const previewButton = renderer.root.find(
@ -226,7 +265,7 @@ describe("ServicesPanelView", () => {
installWindowOpen(windowOpenSpy);
const renderer = renderView({
servicesQuery: { ...makeIdleQuery(), data: { data: [service] } },
servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) },
previewMutation: { trigger },
});
const previewButton = renderer.root.find(

View file

@ -1,6 +1,9 @@
import { useState } from "react";
import { ArrowPathIcon } from "@heroicons/react/20/solid";
import type { SandboxService } from "@qltysh/fabro-api-client";
import type {
SandboxService,
SandboxServiceListResponse,
} from "@qltysh/fabro-api-client";
import { useSandboxServices } from "../../lib/queries";
import { usePreviewRun } from "../../lib/mutations";
@ -10,7 +13,7 @@ import {
ErrorState,
LoadingState,
} from "../../components/state";
import { SECONDARY_BUTTON_CLASS, Tooltip } from "../../components/ui";
import { Tooltip } from "../../components/ui";
export interface ServicesPanelProps {
runId: string;
@ -20,6 +23,9 @@ export interface ServicesPanelProps {
const REFRESH_BUTTON_CLASS =
"inline-flex size-9 items-center justify-center rounded-lg text-fg-2 outline-1 -outline-offset-1 outline-white/10 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-50";
const PREVIEW_BUTTON_CLASS =
"inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium text-fg-2 outline-1 -outline-offset-1 outline-white/10 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-60";
export default function ServicesPanel({ runId, leading }: ServicesPanelProps) {
const servicesQuery = useSandboxServices(runId);
const previewMutation = usePreviewRun(runId);
@ -34,7 +40,7 @@ export default function ServicesPanel({ runId, leading }: ServicesPanelProps) {
}
export interface ServicesQueryShape {
data?: { data: SandboxService[] } | undefined;
data?: SandboxServiceListResponse | undefined;
error?: unknown;
isLoading: boolean;
isValidating: boolean;
@ -71,6 +77,7 @@ export function ServicesPanelView({
const [previewError, setPreviewError] = useState<string | null>(null);
const services = servicesQuery.data?.data ?? [];
const discoverySource = servicesQuery.data?.meta.source;
const queryErrorMessage = describeQueryError(servicesQuery.error);
const showLoading = servicesQuery.isLoading && !servicesQuery.data;
const showError = queryErrorMessage !== null && !servicesQuery.data;
@ -142,11 +149,14 @@ export function ServicesPanelView({
) : services.length === 0 ? (
<EmptyState title="No services" />
) : (
<ServicesTable
services={services}
pendingPort={pendingPort}
onPreview={handlePreview}
/>
<>
{discoverySource === "procfs" ? <ProcfsDiscoveryTip /> : null}
<ServicesTable
services={services}
pendingPort={pendingPort}
onPreview={handlePreview}
/>
</>
)}
</div>
</section>
@ -160,6 +170,17 @@ function describeQueryError(error: unknown): string | null {
return "Could not load services.";
}
function ProcfsDiscoveryTip() {
return (
<div className="mb-3 rounded-md border border-line bg-panel/60 px-3 py-2 text-xs leading-5 text-fg-3">
<span className="font-medium text-fg-2">Tip:</span>{" "}
Install <code className="font-mono text-fg-2">ss</code> in the sandbox
for improved services listing:{" "}
<code className="font-mono text-fg-2">apt-get install iproute2</code>
</div>
);
}
function ServicesTable({
services,
pendingPort,
@ -174,10 +195,11 @@ function ServicesTable({
<table className="w-full text-sm">
<thead className="border-b border-line bg-panel/60">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-fg-3">Port</th>
<th className="px-4 py-2 text-left text-xs font-medium text-fg-3">Bindings</th>
<th className="px-4 py-2 text-left text-xs font-medium text-fg-3">Process</th>
<th className="px-4 py-2 text-right text-xs font-medium text-fg-3">Action</th>
<th className="px-4 py-3 text-left text-xs font-medium text-fg-3">Port</th>
<th className="px-4 py-3 text-left text-xs font-medium text-fg-3">Bindings</th>
<th className="px-4 py-3 text-left text-xs font-medium text-fg-3">Process</th>
<th className="px-4 py-3 text-right text-xs font-medium text-fg-3" />
</tr>
</thead>
<tbody className="divide-y divide-line">
@ -206,34 +228,32 @@ function ServiceRow({
}) {
return (
<tr>
<td className="px-4 py-2 font-mono text-xs text-fg-2">{service.port}</td>
<td className="px-4 py-2 font-mono text-xs text-fg-3">
<td className="px-4 py-3 font-mono text-xs text-fg-2">{service.port}</td>
<td className="px-4 py-3 font-mono text-xs text-fg-3">
{service.addresses.length > 0 ? (
service.addresses.join(", ")
) : (
<span className="text-fg-muted">-</span>
)}
</td>
<td className="px-4 py-2 font-mono text-xs text-fg-3">
<td className="px-4 py-3 font-mono text-xs text-fg-3">
{service.processes.length > 0 ? (
service.processes.join(", ")
) : (
<span className="text-fg-muted">-</span>
)}
</td>
<td className="px-4 py-2 text-right">
<td className="px-4 py-3 text-right">
{service.preview_supported ? (
<button
type="button"
className={SECONDARY_BUTTON_CLASS}
className={PREVIEW_BUTTON_CLASS}
onClick={() => onPreview(service.port)}
disabled={pending}
>
{pending ? "Opening…" : "Preview"}
</button>
) : (
<span className="text-xs text-fg-muted">Unavailable</span>
)}
) : null}
</td>
</tr>
);

View file

@ -8004,13 +8004,13 @@ components:
example: 3000
addresses:
type: array
description: Local bind addresses reported by `ss`.
description: Local bind addresses discovered from `ss` or `/proc/net/tcp*`.
items:
type: string
example: ["127.0.0.1:3000", "[::]:3000"]
processes:
type: array
description: Visible process summaries reported by `ss`.
description: Visible process summaries when available. Empty when the sandbox only supports `/proc/net/tcp*` discovery.
items:
type: string
example: ['users:(("node",pid=42,fd=23))']
@ -8024,11 +8024,30 @@ components:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/SandboxService"
meta:
$ref: "#/components/schemas/SandboxServiceListMeta"
SandboxServiceListMeta:
description: Metadata about sandbox service discovery.
type: object
required:
- source
properties:
source:
$ref: "#/components/schemas/SandboxServiceDiscoverySource"
SandboxServiceDiscoverySource:
description: Tool or kernel interface used to discover sandbox services.
type: string
enum:
- ss
- procfs
VncPreviewResponse:
description: Response containing a signed noVNC preview URL for a Daytona sandbox.

View file

@ -386,6 +386,16 @@ fn main() {
("GitContext", "fabro_types::GitContext", &[]),
("SandboxDetails", "fabro_types::SandboxDetails", &[]),
("SandboxService", "fabro_types::SandboxService", &[]),
(
"SandboxServiceDiscoverySource",
"fabro_types::SandboxServiceDiscoverySource",
&[],
),
(
"SandboxServiceListMeta",
"fabro_types::SandboxServiceListMeta",
&[],
),
(
"SandboxServiceListResponse",
"fabro_types::SandboxServiceListResponse",

View file

@ -4,7 +4,10 @@ use fabro_api::types::{
SandboxService as ApiSandboxService,
SandboxServiceListResponse as ApiSandboxServiceListResponse,
};
use fabro_types::{SandboxService, SandboxServiceListResponse};
use fabro_types::{
SandboxService, SandboxServiceDiscoverySource, SandboxServiceListMeta,
SandboxServiceListResponse,
};
use serde_json::json;
#[test]
@ -25,6 +28,9 @@ fn sandbox_services_json_matches_openapi_shape() {
],
preview_supported: true,
}],
meta: SandboxServiceListMeta {
source: SandboxServiceDiscoverySource::Ss,
},
};
assert_eq!(
@ -38,17 +44,22 @@ fn sandbox_services_json_matches_openapi_shape() {
r#"users:(("vite",pid=84,fd=19))"#,
],
"preview_supported": true
}]
}],
"meta": {
"source": "ss"
}
})
);
}
#[test]
fn sandbox_services_deserializes_empty_response() {
let response: SandboxServiceListResponse = serde_json::from_value(json!({ "data": [] }))
.expect("empty service response should deserialize");
let response: SandboxServiceListResponse =
serde_json::from_value(json!({ "data": [], "meta": { "source": "procfs" } }))
.expect("empty service response should deserialize");
assert!(response.data.is_empty());
assert_eq!(response.meta.source, SandboxServiceDiscoverySource::Procfs);
}
fn assert_same_type<T: 'static, U: 'static>() {

View file

@ -23,6 +23,7 @@ use fabro_api::types::{
RunFilesMeta, RunFilesMetaScope, RunFilesMetaSource, SandboxService,
SandboxServiceListResponse,
};
use fabro_types::{SandboxServiceDiscoverySource, SandboxServiceListMeta};
use serde_json::json;
use crate::error::ApiError;
@ -423,6 +424,9 @@ pub(crate) async fn list_sandbox_services_stub(
preview_supported: false,
},
],
meta: SandboxServiceListMeta {
source: SandboxServiceDiscoverySource::Ss,
},
}),
)
.into_response()

View file

@ -1,9 +1,11 @@
use std::collections::BTreeMap;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::num::NonZeroU64;
use std::sync::Arc;
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
use fabro_sandbox::{TerminalSize, open_terminal_for_run};
use fabro_types::{SandboxServiceDiscoverySource, SandboxServiceListMeta};
use futures_util::FutureExt;
use futures_util::future::BoxFuture;
@ -20,7 +22,19 @@ use super::super::{
const MAX_TERMINAL_CONTROL_BYTES: usize = 4096;
const DEFAULT_VNC_NO_VNC_PORT: u16 = 6080;
const DEFAULT_VNC_TTL_SECS: i32 = 3600;
const LIST_SANDBOX_SERVICES_COMMAND: &str = "ss -H -ltnp";
const LIST_SANDBOX_SERVICES_COMMAND: &str = r#"if command -v ss >/dev/null 2>&1; then
ss -H -ltnp && exit 0
fi
printf 'FABRO_PROC_NET_TCP procfs\n'
for file in /proc/net/tcp /proc/net/tcp6; do
if [ -r "$file" ]; then
printf 'FABRO_PROC_NET_TCP %s\n' "$file"
while IFS= read -r line; do
printf '%s\n' "$line"
done < "$file"
fi
done"#;
const LIST_SANDBOX_SERVICES_FAILURE_LABEL: &str = "sandbox service discovery command";
const LIST_SANDBOX_SERVICES_TIMEOUT_MS: u64 = 5_000;
// Daytona's signed preview points at the noVNC service root, which serves a
// directory listing. Force the iframe to the actual viewer page with
@ -572,8 +586,12 @@ async fn list_sandbox_services(
.into_response();
}
let discovery = parse_sandbox_services(&result.stdout, &provider);
Json(SandboxServiceListResponse {
data: parse_ss_listening_services(&result.stdout, &provider),
data: discovery.services,
meta: SandboxServiceListMeta {
source: discovery.source,
},
})
.into_response()
}
@ -587,7 +605,29 @@ fn sandbox_service_command_failure_detail(result: &fabro_sandbox::ExecResult) ->
if !stdout.is_empty() {
return stdout.to_string();
}
format!("{LIST_SANDBOX_SERVICES_COMMAND} failed")
format!("{LIST_SANDBOX_SERVICES_FAILURE_LABEL} failed")
}
struct SandboxServiceDiscovery {
services: Vec<SandboxService>,
source: SandboxServiceDiscoverySource,
}
fn parse_sandbox_services(output: &str, provider: &str) -> SandboxServiceDiscovery {
if output
.lines()
.any(|line| line.trim_start().starts_with("FABRO_PROC_NET_TCP "))
{
SandboxServiceDiscovery {
services: parse_proc_net_listening_services(output, provider),
source: SandboxServiceDiscoverySource::Procfs,
}
} else {
SandboxServiceDiscovery {
services: parse_ss_listening_services(output, provider),
source: SandboxServiceDiscoverySource::Ss,
}
}
}
fn parse_ss_listening_services(output: &str, provider: &str) -> Vec<SandboxService> {
@ -605,18 +645,9 @@ fn parse_ss_listening_services(output: &str, provider: &str) -> Vec<SandboxServi
continue;
};
let process = (fields.len() > 5).then(|| fields[5..].join(" "));
let service = services.entry(port).or_insert_with(|| SandboxService {
port,
addresses: Vec::new(),
processes: Vec::new(),
preview_supported: preview_supported(provider, port),
});
push_unique(&mut service.addresses, address.to_string());
if let Some(process) = process {
push_unique(&mut service.processes, process);
}
push_service(&mut services, provider, port, address.to_string(), process);
}
services.into_values().collect()
sorted_services(services)
}
fn parse_ss_local_port(address: &str) -> Option<u16> {
@ -624,6 +655,108 @@ fn parse_ss_local_port(address: &str) -> Option<u16> {
(port > 0).then_some(port)
}
#[derive(Clone, Copy)]
enum ProcNetFamily {
Ipv4,
Ipv6,
}
fn parse_proc_net_listening_services(output: &str, provider: &str) -> Vec<SandboxService> {
let mut services = BTreeMap::<u16, SandboxService>::new();
let mut family = None;
for line in output
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
{
if let Some(path) = line.strip_prefix("FABRO_PROC_NET_TCP ") {
family = if path.ends_with("/tcp6") {
Some(ProcNetFamily::Ipv6)
} else {
Some(ProcNetFamily::Ipv4)
};
continue;
}
if line.starts_with("sl") {
continue;
}
let Some(family) = family else {
continue;
};
let fields = line.split_whitespace().collect::<Vec<_>>();
let (Some(local_address), Some(state)) = (fields.get(1), fields.get(3)) else {
continue;
};
if *state != "0A" {
continue;
}
let Some((address, port)) = parse_proc_net_local_address(local_address, family) else {
continue;
};
push_service(&mut services, provider, port, address, None);
}
sorted_services(services)
}
fn sorted_services(services: BTreeMap<u16, SandboxService>) -> Vec<SandboxService> {
let mut services = services.into_values().collect::<Vec<_>>();
services.sort_by_key(|service| (!service.preview_supported, service.port));
services
}
fn parse_proc_net_local_address(value: &str, family: ProcNetFamily) -> Option<(String, u16)> {
let (address_hex, port_hex) = value.split_once(':')?;
let port = u16::from_str_radix(port_hex, 16).ok()?;
if port == 0 {
return None;
}
let address = match family {
ProcNetFamily::Ipv4 => format!("{}:{port}", parse_proc_net_ipv4(address_hex)?),
ProcNetFamily::Ipv6 => format!("[{}]:{port}", parse_proc_net_ipv6(address_hex)?),
};
Some((address, port))
}
fn parse_proc_net_ipv4(value: &str) -> Option<Ipv4Addr> {
if value.len() != 8 {
return None;
}
let raw = u32::from_str_radix(value, 16).ok()?;
Some(Ipv4Addr::from(raw.to_le_bytes()))
}
fn parse_proc_net_ipv6(value: &str) -> Option<Ipv6Addr> {
if value.len() != 32 {
return None;
}
let mut bytes = [0_u8; 16];
for (chunk_index, chunk) in value.as_bytes().chunks_exact(8).enumerate() {
let chunk = std::str::from_utf8(chunk).ok()?;
let raw = u32::from_str_radix(chunk, 16).ok()?;
bytes[chunk_index * 4..chunk_index * 4 + 4].copy_from_slice(&raw.to_le_bytes());
}
Some(Ipv6Addr::from(bytes))
}
fn push_service(
services: &mut BTreeMap<u16, SandboxService>,
provider: &str,
port: u16,
address: String,
process: Option<String>,
) {
let service = services.entry(port).or_insert_with(|| SandboxService {
port,
addresses: Vec::new(),
processes: Vec::new(),
preview_supported: preview_supported(provider, port),
});
push_unique(&mut service.addresses, address);
if let Some(process) = process {
push_unique(&mut service.processes, process);
}
}
fn preview_supported(provider: &str, port: u16) -> bool {
provider == SandboxProvider::Daytona.to_string() && (3000..=9999).contains(&port)
}
@ -868,24 +1001,24 @@ LISTEN 0 4096 [::1]:2500 [::]:* users:(("debug",pid=168,fd=7))
);
assert_eq!(services.len(), 4);
assert_eq!(services[0].port, 2500);
assert_eq!(services[0].addresses, vec!["[::1]:2500"]);
assert_eq!(services[0].port, 3000);
assert_eq!(services[0].addresses, vec!["127.0.0.1:3000"]);
assert_eq!(services[0].processes, vec![
r#"users:(("debug",pid=168,fd=7))"#
]);
assert!(!services[0].preview_supported);
assert_eq!(services[1].port, 3000);
assert_eq!(services[1].addresses, vec!["127.0.0.1:3000"]);
assert_eq!(services[1].processes, vec![
r#"users:(("node",pid=42,fd=23))"#
]);
assert!(services[0].preview_supported);
assert_eq!(services[1].port, 5173);
assert_eq!(services[1].addresses, vec!["0.0.0.0:5173"]);
assert!(services[1].preview_supported);
assert_eq!(services[2].port, 5173);
assert_eq!(services[2].addresses, vec!["0.0.0.0:5173"]);
assert_eq!(services[2].port, 8080);
assert_eq!(services[2].addresses, vec!["[::]:8080"]);
assert!(services[2].preview_supported);
assert_eq!(services[3].port, 8080);
assert_eq!(services[3].addresses, vec!["[::]:8080"]);
assert!(services[3].preview_supported);
assert_eq!(services[3].port, 2500);
assert_eq!(services[3].addresses, vec!["[::1]:2500"]);
assert_eq!(services[3].processes, vec![
r#"users:(("debug",pid=168,fd=7))"#
]);
assert!(!services[3].preview_supported);
}
#[test]
@ -931,6 +1064,52 @@ LISTEN 0 4096 [::]:3000 [::]:* users:(("vite",pid=84,fd=19))
}]);
}
#[test]
fn proc_net_parser_extracts_listening_tcp_services_without_processes() {
let discovery = parse_sandbox_services(
r#"
FABRO_PROC_NET_TCP /proc/net/tcp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:0BB8 00000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 11111
1: 00000000:1435 00000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 22222
2: 0100007F:2328 00000000:0000 01 00000000:00000000 00:00000000 00000000 501 0 33333
FABRO_PROC_NET_TCP /proc/net/tcp6
sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000000000000000000000000000:1F90 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 44444
1: 00000000000000000000000001000000:09C4 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 55555
"#,
"daytona",
);
assert_eq!(discovery.source, SandboxServiceDiscoverySource::Procfs);
assert_eq!(discovery.services, vec![
SandboxService {
port: 3000,
addresses: vec!["127.0.0.1:3000".to_string()],
processes: vec![],
preview_supported: true,
},
SandboxService {
port: 5173,
addresses: vec!["0.0.0.0:5173".to_string()],
processes: vec![],
preview_supported: true,
},
SandboxService {
port: 8080,
addresses: vec!["[::]:8080".to_string()],
processes: vec![],
preview_supported: true,
},
SandboxService {
port: 2500,
addresses: vec!["[::1]:2500".to_string()],
processes: vec![],
preview_supported: false,
},
]);
}
#[test]
fn preview_support_is_daytona_only_for_documented_range() {
assert!(!preview_supported("daytona", 2500));
@ -963,7 +1142,7 @@ LISTEN 0 4096 [::]:3000 [::]:* users:(("vite",pid=84,fd=19))
result.stdout.clear();
assert_eq!(
sandbox_service_command_failure_detail(&result),
"ss -H -ltnp failed"
"sandbox service discovery command failed"
);
}

View file

@ -81,7 +81,10 @@ pub use run_summary::RunSummary;
pub use run_title::{RunTitleError, infer_run_title, normalize_explicit_run_title};
pub use sandbox_details::{SandboxDetails, SandboxResources, SandboxState, SandboxTimestamps};
pub use sandbox_record::SandboxRecord;
pub use sandbox_services::{SandboxService, SandboxServiceListResponse};
pub use sandbox_services::{
SandboxService, SandboxServiceDiscoverySource, SandboxServiceListMeta,
SandboxServiceListResponse,
};
pub use secret::{SecretMetadata, SecretType};
pub use stage_completion::StageCompletion;
pub use stage_handler::StageHandler;

View file

@ -1,3 +1,15 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SandboxServiceDiscoverySource {
Ss,
Procfs,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SandboxServiceListMeta {
pub source: SandboxServiceDiscoverySource,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SandboxService {
pub port: u16,
@ -9,4 +21,5 @@ pub struct SandboxService {
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SandboxServiceListResponse {
pub data: Vec<SandboxService>,
pub meta: SandboxServiceListMeta,
}

View file

@ -290,6 +290,8 @@ models/sandbox-details.ts
models/sandbox-file-entry.ts
models/sandbox-file-list-response.ts
models/sandbox-resources.ts
models/sandbox-service-discovery-source.ts
models/sandbox-service-list-meta.ts
models/sandbox-service-list-response.ts
models/sandbox-service.ts
models/sandbox-state.ts

View file

@ -268,6 +268,8 @@ export * from './sandbox-file-entry';
export * from './sandbox-file-list-response';
export * from './sandbox-resources';
export * from './sandbox-service';
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';

View file

@ -0,0 +1,29 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Tool or kernel interface used to discover sandbox services.
*/
export const SandboxServiceDiscoverySource = {
SS: 'ss',
PROCFS: 'procfs'
} as const;
export type SandboxServiceDiscoverySource = typeof SandboxServiceDiscoverySource[keyof typeof SandboxServiceDiscoverySource];

View file

@ -0,0 +1,28 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.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 { SandboxServiceDiscoverySource } from './sandbox-service-discovery-source';
/**
* Metadata about sandbox service discovery.
*/
export interface SandboxServiceListMeta {
'source': SandboxServiceDiscoverySource;
}

View file

@ -16,11 +16,15 @@
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxService } from './sandbox-service';
// May contain unused imports in some cases
// @ts-ignore
import type { SandboxServiceListMeta } from './sandbox-service-list-meta';
/**
* Non-paginated list of listening TCP services in a run sandbox.
*/
export interface SandboxServiceListResponse {
'data': Array<SandboxService>;
'meta': SandboxServiceListMeta;
}

View file

@ -23,11 +23,11 @@ export interface SandboxService {
*/
'port': number;
/**
* Local bind addresses reported by `ss`.
* Local bind addresses discovered from `ss` or `/proc/net/tcp*`.
*/
'addresses': Array<string>;
/**
* Visible process summaries reported by `ss`.
* Visible process summaries when available. Empty when the sandbox only supports `/proc/net/tcp*` discovery.
*/
'processes': Array<string>;
/**