Port now publishes a supported typed client surface in the port-sdk crate for
hosted machine, guest, and service operations.
The crate now covers both typed request construction and live JSON response execution against the hosted control-plane transport that Port ships for the single-node demo lane.
Shipped today:
HostedClient::from_machinederives hosted endpoint, audience, and auth header shape from the shared Port model plusport-hosted-protocolHostedClient::from_machine_envandHostedClient::from_control_plane_envderive the auth token source from the model and read the configured environment variable automaticallymachines()mirrorsport machine list|status|monitor|top|stopguest()mirrorsport guest exec|copy|pty|logs|forwardusing the existingport-agent-protocolrequest payloadsguest().pty_stream(),logs_stream(),copy_stream(), andforward_stream()publish the streamed hosted request builders when the caller needs explicit control over the transport contract instead of the completed JSON helperguest().forward_detached_start(),forward_detached_list(), andforward_detached_stop()mirror hostedport guest forward --lifecycle detached,--list, and--stop --name ...through the same control-plane route familyservices()mirrorsport service secret put|list|removeplusport service apply|list|status|stop- those service calls execute through the same live hosted control-plane and node-agent path as the CLI demo lane; they do not define a second hosted-only service surface
- managed guest-process
start|list|status|stopstays internal to the shared guest/runtime contract, so the SDK does not add a second hosted-only service client family HostedClient::execute_jsonperforms the live HTTP request and decodes structured success or hosted route errorsport-hosted-protocolpublishes the shared hosted HTTP route, auth-header, and route-context contract that the SDK now uses directly- hosted guest responses surface
HostedSuccess::route.guest_session, which carries the stable machine-scoped session identifier plus the canonical Port shell-driver metadata for guest-backed hostedexec,copy,pty,logs, andforward guest().shell_driver_contract(machine)derives that same canonical machine-scoped shell-driver contract ahead of time for streamedptyandforwardintegrations that need a stable identity before opening the hosted byte stream
Still planned:
- retries and richer client policies on top of the shipped transport
- generated or versioned external API packages beyond the in-repo Rust crate
- advanced auth, RBAC, and multi-tenant concerns
- external secret-manager integrations and broader orchestration policy beyond the shipped restart/health/runtime-file service contract
use port_agent_protocol::ExecRequest;
use port_model::PortConfig;
use port_sdk::{HostedClient, ServiceApplyRequest, ServiceKind, ServiceSecretBinding};
let config = PortConfig::sample();
let client = HostedClient::from_machine(&config, "cloud-aws", "demo-token")?;
let status = client.machines().status("cloud-aws");
let exec = client.guest().exec(
"cloud-aws",
ExecRequest {
command: vec!["/bin/echo".into(), "hello".into()],
cwd: None,
env: Default::default(),
},
)?;
let service = client.services().apply(
"cloud-aws",
ServiceApplyRequest {
name: "buildbox".into(),
kind: ServiceKind::Sandbox,
command: vec!["/bin/sh".into(), "-lc".into(), "make test".into()],
secret_bindings: vec![ServiceSecretBinding {
env: "API_TOKEN".into(),
secret: "demo-token".into(),
}],
},
)?;
assert_eq!(status.url, "https://port.example.internal/v1/machines/cloud-aws");
assert_eq!(exec.url, "https://port.example.internal/v1/machines/cloud-aws/guest:exec");
assert_eq!(service.url, "https://port.example.internal/v1/machines/cloud-aws/services");
# Ok::<(), anyhow::Error>(())If a hosted control plane is running and the token source is configured in the environment, the same client can execute the request directly:
# use port_hosted_protocol::HostedSuccess;
# use port_sdk::HostedClient;
# use port_model::PortConfig;
let config = PortConfig::sample();
let client = HostedClient::from_machine_env(&config, "cloud-aws")?;
let status: HostedSuccess<serde_json::Value> =
client.execute_json(client.machines().status("cloud-aws"))?;
# Ok::<(), anyhow::Error>(())Hosted guest requests expose the session and driver contract through the same route context:
# use port_agent_protocol::ExecRequest;
# use port_hosted_protocol::HostedSuccess;
# use port_model::PortConfig;
# use port_sdk::HostedClient;
let config = PortConfig::sample();
let client = HostedClient::from_machine(&config, "cloud-aws", "demo-token")?;
let exec: HostedSuccess<serde_json::Value> = client.execute_json(
client.guest().exec(
"cloud-aws",
ExecRequest {
command: vec!["/bin/echo".into(), "hello".into()],
cwd: None,
env: Default::default(),
},
)?,
)?;
let session = exec
.route
.guest_session
.expect("hosted guest session contract");
assert_eq!(session.id, "port-hosted://demo/machines/cloud-aws/guest-session");
assert_eq!(session.driver.id, "port-guest-shell-driver-v1");
# Ok::<(), anyhow::Error>(())For streamed hosted requests, derive the same contract before opening the transport:
# use port_model::PortConfig;
# use port_sdk::HostedClient;
let config = PortConfig::sample();
let client = HostedClient::from_machine(&config, "cloud-aws", "demo-token")?;
let session = client.guest().shell_driver_contract("cloud-aws")?;
assert_eq!(session.id, "port-hosted://demo/machines/cloud-aws/guest-session");
assert_eq!(session.driver.id, "port-guest-shell-driver-v1");
# Ok::<(), anyhow::Error>(())That helper is the canonical upstream shell-driver contract for hosted guest integration:
| Operation | Canonical Port surface | Lifecycle model | Stable contract surface |
|---|---|---|---|
exec |
guest().exec() and port guest exec |
Request/response via HostedClient::execute_json |
HostedSuccess::route.guest_session on hosted responses |
pty |
guest().pty_stream() and port guest pty |
Bidirectional stream until guest exit | guest().shell_driver_contract(machine) before stream open |
forward |
guest().forward_stream() or detached helpers and port guest forward |
Byte stream or detached listener lifecycle | guest().shell_driver_contract(machine) before start/list/stop |
The helper is only available when the client is created from Port control-plane
configuration, because generic HostedClient::new(...) callers do not know the
stable control-plane name needed for the session identifier.
See crates/port-sdk/examples/hosted-sdk.rs for the in-repo sample program.
The streamed hosted request builders stay explicit:
# use port_agent_protocol::{ForwardRequest, LogsRequest};
# use port_hosted_protocol::HostedDetachedForwardStartRequest;
# use port_model::PortConfig;
# use port_sdk::HostedClient;
let config = PortConfig::sample();
let client = HostedClient::from_machine(&config, "cloud-aws", "demo-token")?;
let logs = client.guest().logs_stream(
"cloud-aws",
LogsRequest {
path: "/var/log/app.log".into(),
follow: true,
tail_lines: None,
},
)?;
let forward = client.guest().forward_stream(
"cloud-aws",
ForwardRequest {
listen: "127.0.0.1:8081".into(),
target: "127.0.0.1:80".into(),
},
)?;
let detached = client.guest().forward_detached_start(
"cloud-aws",
HostedDetachedForwardStartRequest {
listen: "unix:/tmp/cloud-aws.sock".into(),
target: "unix:/var/run/app.sock".into(),
forward_name: "demo-sock".into(),
},
)?;
let detached_list = client.guest().forward_detached_list("cloud-aws");
let detached_stop = client
.guest()
.forward_detached_stop("cloud-aws", "demo-sock");
assert_eq!(logs.request.url, "https://port.example.internal/v1/machines/cloud-aws/guest:logs:stream");
assert_eq!(forward.request.url, "https://port.example.internal/v1/machines/cloud-aws/guest:forward:stream");
assert_eq!(detached.url, "https://port.example.internal/v1/machines/cloud-aws/guest:forward:detached");
assert_eq!(detached_list.url, "https://port.example.internal/v1/machines/cloud-aws/guest:forward:detached");
assert_eq!(detached_stop.url, "https://port.example.internal/v1/machines/cloud-aws/guest:forward:detached/demo-sock/stop");
# Ok::<(), anyhow::Error>(())HostedClient::execute_json remains the live helper for the shipped JSON
machine, guest, and service routes. Stream requests stay typed and explicit so
the caller owns any long-lived byte-stream transport policy layered on top.
Canonical hosted request paths now documented by Port:
GET /v1/machinesGET /v1/machines/{machine}GET /v1/machines/{machine}/monitorGET /v1/machines/{machine}/topPOST /v1/machines/{machine}:stopPOST /v1/machines/{machine}/guest:exec|copy|pty|logs|forwardPUT /v1/machines/{machine}/secrets/{secret}GET /v1/machines/{machine}/secretsDELETE /v1/machines/{machine}/secrets/{secret}POST /v1/machines/{machine}/servicesGET /v1/machines/{machine}/servicesGET /v1/machines/{machine}/services/{name}POST /v1/machines/{machine}/services/{name}:stop
The SDK mirrors those paths exactly so later transport work can build on a
stable typed surface instead of inventing a second client model. The route and
header definitions themselves live in crates/port-hosted-protocol.